├── .coveragerc ├── .coveralls.yml ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── .keep │ ├── _templates │ └── .keep │ ├── conf.py │ ├── context-builder.rst │ ├── datastore.rst │ ├── django-app.rst │ ├── flask-app.rst │ ├── images │ ├── admin1.png │ ├── admin2.png │ ├── detail-page.png │ └── home.png │ ├── index.rst │ ├── mask-rule.rst │ ├── notification.rst │ └── ticketing.rst ├── error_tracker ├── __init__.py ├── django │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── middleware.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20201018_1311.py │ │ └── __init__.py │ ├── models.py │ ├── settings.py │ ├── templates │ │ └── error_tracker │ │ │ ├── admin │ │ │ └── change_form.html │ │ │ ├── base.html │ │ │ ├── detail.html │ │ │ ├── list.html │ │ │ └── partials │ │ │ ├── navigation.html │ │ │ └── partial_table.html │ ├── templatetags │ │ ├── __init__.py │ │ └── error_tracker.py │ ├── urls.py │ ├── utils.py │ └── views.py ├── flask │ ├── __init__.py │ ├── defaults.py │ ├── flask_error.py │ ├── templates │ │ └── error_tracker │ │ │ ├── base.html │ │ │ ├── detail.html │ │ │ └── list.html │ ├── utils.py │ └── view.py └── libs │ ├── __init__.py │ ├── exception_formatter.py │ ├── mixins.py │ └── utils.py ├── examples ├── DjangoSample │ ├── DjangoSample │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── core │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── tests.py │ │ ├── utils.py │ │ └── views.py │ └── manage.py └── flask-sample │ ├── app.py │ ├── settings.py │ └── templates │ ├── 401.html │ ├── 500.html │ └── home.html ├── locale └── fr_FR │ └── LC_MESSAGES │ └── django.po ├── requirements-dev.txt ├── run-tests.sh ├── setup.cfg ├── setup.py └── tests ├── DjangoTest ├── DjangoTest │ ├── __init__.py │ ├── custom_model_settings.py │ ├── django_rest_framework_settings.py │ ├── drf_urls.py │ ├── masking_custom_settings.py │ ├── masking_disabled_settings.py │ ├── notification_settings.py │ ├── settings.py │ ├── urls.py │ ├── view_401_settings.py │ └── wsgi.py ├── __init__.py ├── core │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── serializers.py │ └── views.py ├── manage.py ├── runners │ ├── __init__.py │ ├── base_test_runner.py │ ├── common_runner.py │ ├── custom_masking_test_runner.py │ ├── drf_test_runner.py │ ├── masking_disabled_test_runner.py │ ├── model_test_runner.py │ ├── notification_test_runner.py │ └── view_401_test_runner.py └── tests │ ├── __init__.py │ ├── test_401_end_point.py │ ├── test_basic.py │ ├── test_custom_masking.py │ ├── test_db_model.py │ ├── test_drf.py │ ├── test_end_point.py │ ├── test_manual_error_tracking.py │ ├── test_mask_rule.py │ ├── test_no_masking.py │ ├── test_notification.py │ ├── test_notification_disabled.py │ ├── test_ticketing.py │ └── util.py ├── FlaskTest ├── __init__.py ├── configs │ ├── __init__.py │ ├── custom_mask_rule.py │ ├── masking_disabled.py │ ├── notification_config_disabled.py │ ├── notification_config_enabled.py │ └── pagination_config.py ├── test_401_views.py ├── test_basic.py ├── test_db_model.py ├── test_end_point.py ├── test_init_later.py ├── test_manager_crud.py ├── test_manual_error_tracking.py ├── test_mask_rule.py ├── test_notification.py ├── test_ticketing.py ├── test_url_prefix.py └── utils.py ├── __init__.py ├── flask-test-runner.py └── utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = error_tracker/* 3 | omit = *migrations*, *tests* 4 | 5 | [report] 6 | exclude_lines = 7 | pragma: no cover 8 | def __repr__ 9 | raise NotImplementedError 10 | if __name__ == .__main__.: -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/.coveralls.yml -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sonus21 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### VirtualEnv template 3 | # Virtualenv 4 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 5 | .Python 6 | [Bb]in 7 | [Ii]nclude 8 | [Ll]ib 9 | [Ll]ib64 10 | [Ll]ocal 11 | [Ss]cripts 12 | pyvenv.cfg 13 | .venv 14 | pip-selfcheck.json 15 | ### Python template 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[cod] 19 | *$py.class 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | *.sqlite* 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | env/ 101 | venv/ 102 | ENV/ 103 | env.bak/ 104 | venv.bak/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | .idea 119 | .vscode/settings.json 120 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - 2.7 5 | - 3.5 6 | - 3.6 7 | - 3.7 8 | - 3.8 9 | # - pypy 10 | # - pypy3 11 | cache: pip 12 | install: 13 | - pip install -r requirements-dev.txt 14 | 15 | script: 16 | - coverage run tests/DjangoTest/runners/common_runner.py 17 | - coverage run -a tests/DjangoTest/runners/drf_test_runner.py 18 | - coverage run -a tests/DjangoTest/runners/custom_masking_test_runner.py 19 | - coverage run -a tests/DjangoTest/runners/masking_disabled_test_runner.py 20 | - coverage run -a tests/DjangoTest/runners/model_test_runner.py 21 | - coverage run -a tests/DjangoTest/runners/notification_test_runner.py 22 | - coverage run -a tests/flask-test-runner.py 23 | 24 | notifications: 25 | email: 26 | - sonunitw12@gmail.com 27 | 28 | after_success: 29 | coveralls 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019-2023 Sonu Kumar 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.rst 3 | recursive-include error_tracker/* * 4 | recursive-exclude error_tracker *.pyc -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Error Tracker 3 | ============= 4 | 5 | **Full featured error tracking module for Python apps supports Flask and Django** 6 | 7 | .. image:: https://img.shields.io/pypi/v/error-tracker.svg?color=dark-green 8 | :target: https://pypi.org/project/error-tracker 9 | 10 | .. image:: https://img.shields.io/pypi/pyversions/error-tracker.svg?color=dark-green 11 | :target: https://pypi.org/project/error-tracker 12 | 13 | .. image:: https://img.shields.io/github/license/sonus21/error-tracker.svg?color=dark-green 14 | :target: https://github.com/sonus21/error-tracker/blob/master/LICENSE.txt 15 | 16 | .. image:: https://travis-ci.org/sonus21/error-tracker.svg?branch=master 17 | :target: https://travis-ci.org/sonus21/error-tracker 18 | 19 | .. image:: https://coveralls.io/repos/github/sonus21/error-tracker/badge.svg?color=dark-green 20 | :target: https://coveralls.io/github/sonus21/error-tracker 21 | 22 | Introduction 23 | ------------ 24 | ErrorTracker is a batteries-included app and extensions for python app, that can track errors, send notification, mask sensitive data and capture frames data. 25 | 26 | It plays nicely with `Django `_ and `Flask `_ 27 | 28 | Simple to use extension that lets you add error recording interfaces to Python applications. 29 | It's implemented in such a way that the developer has total control of the resulting application. 30 | 31 | Out-of-the-box, Error Tracker plays nicely with various ORM's, including 32 | 33 | - `SQLAlchemy `_, 34 | - `MongoEngine `_, 35 | - `Django ORM `_ 36 | 37 | 38 | It also boasts a simple Model management interface. 39 | 40 | The biggest feature of ErrorTracker is flexibility. To start off with you can create a very simple application in no time, 41 | with exception monitor enabled, but then you can go further and customize different aspects. 42 | 43 | ErrorTracker is an active project, well-tested and production ready. 44 | 45 | Installation 46 | ------------ 47 | To install ErrorTracker, simply:: 48 | 49 | pip install error-tracker 50 | 51 | 52 | Features 53 | -------- 54 | - Sensitive data( like *password*, *secret* ) Masking 55 | - Record all the frames ( frame data are stored in JSON format so that it can be analyzed later) 56 | - Unique URL generation 57 | - Number of times the exception occurred and first/last time of exception 58 | - Sending notifications with exception details 59 | - Record different types of exception like 500 or 404 etc 60 | - Raise or update ticket in Jira/Bugzilla etc by ticketing interface. 61 | 62 | Usage 63 | ----- 64 | 65 | Flask App configuration 66 | ======================= 67 | 68 | .. code:: 69 | 70 | ... 71 | APP_ERROR_SEND_EMAIL = True 72 | APP_ERROR_RECIPIENT_EMAIL = ('example@example.com',) 73 | APP_ERROR_SUBJECT_PREFIX = "Server Error" 74 | APP_ERROR_EMAIL_SENDER = 'user@example.com' 75 | 76 | 77 | 78 | app.py 79 | 80 | .. code:: 81 | 82 | from flask import Flask 83 | from flask_mail import Mail 84 | import settings 85 | from error_tracker import AppErrorTracker, NotificationMixin 86 | from flask_sqlalchemy import SQLAlchemy 87 | ... 88 | app = Flask(__name__) 89 | app.config.from_object(settings) 90 | db = SQLAlchemy(app) 91 | class Notifier(Mail, NotificationMixin): 92 | def notify(self, request, exception, 93 | email_subject=None, 94 | email_body=None, 95 | from_email=None, 96 | recipient_list=None): 97 | message = Message(email_subject, recipient_list, email_body, sender=from_email) 98 | self.send(message) 99 | mailer = Notifier(app=app) 100 | error_tracker = AppErrorTracker(app=app, db=db, notifier=mailer) 101 | 102 | .... 103 | 104 | .... 105 | # Record exception when 404 error code is raised 106 | @app.errorhandler(403) 107 | def error_403(e): 108 | error_tracker.capture_exception() 109 | # any custom logic 110 | 111 | # Record error using decorator 112 | @app.errorhandler(500) 113 | @error_tracker.track_exception 114 | def error_500(e): 115 | # some custom logic 116 | .... 117 | 118 | 119 | Django App Usage 120 | ================ 121 | 122 | We need to update settings.py file as 123 | 124 | - Add app to installed apps list 125 | - Add Middleware for exception tracking. This should be added at the end so that it can process exception 1st in the middleware call stack. 126 | - Other configs related to notification 127 | 128 | Sample Code 129 | 130 | 131 | .. code:: 132 | 133 | ... 134 | APP_ERROR_RECIPIENT_EMAIL = ('example@example.com',) 135 | APP_ERROR_SUBJECT_PREFIX = "Server Error" 136 | APP_ERROR_EMAIL_SENDER = 'user@example.com' 137 | 138 | INSTALLED_APPS = [ 139 | ... 140 | 'error_tracker.DjangoErrorTracker' 141 | ] 142 | MIDDLEWARE = [ 143 | ... 144 | 'error_tracker.django.middleware.ExceptionTrackerMiddleWare' 145 | ] 146 | 147 | 148 | Documentations 149 | -------------- 150 | This has got extensive document browse at https://error-tracker.readthedocs.io/en/latest/ 151 | 152 | All docs are in `docs/source` 153 | 154 | And if you want to preview any *.rst* snippets that you may want to contribute, go to `http://rst.ninjs.org/ `_. 155 | 156 | 157 | Examples 158 | -------- 159 | Several usage examples are included in the */tests* folder. Please feel free to add your own examples, or improve 160 | on some of the existing ones, and then submit them via GitHub as a *pull-request*. 161 | 162 | You can see some of these examples in action at https://github.com/sonus21/error-tracker/tree/master/examples 163 | To run the examples on your local environment, one at a time, do something like:: 164 | 165 | cd error-tracker/examples 166 | 167 | 168 | Django:: 169 | 170 | cd error-tracker/examples 171 | cd DjangoSample 172 | python manage.py runserver 173 | 174 | Flask:: 175 | 176 | cd flask-sample 177 | python app.py 178 | 179 | 180 | Tests 181 | ----- 182 | To run the tests, from the project directory, simply:: 183 | 184 | pip install -r requirements-dev.txt 185 | bash run-tests.sh 186 | 187 | You should see output similar to:: 188 | 189 | ............................................. 190 | ---------------------------------------------------------------------- 191 | Ran 31 tests in 1.144s 192 | 193 | OK 194 | 195 | 196 | Contribution 197 | ------------- 198 | You're most welcome to raise pull request or fixes. 199 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = error-tracker 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=error-tracker 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/_static/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/docs/source/_static/.keep -------------------------------------------------------------------------------- /docs/source/_templates/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/docs/source/_templates/.keep -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | import datetime 18 | 19 | sys.path.insert(0, os.path.abspath('../..')) 20 | import error_tracker 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = u'error-tracker' 25 | copyright = str(datetime.datetime.now().year) + u', Sonu Kumar' 26 | author = error_tracker.__author__ 27 | 28 | # The short X.Y version 29 | version = u'' 30 | # The full version, including alpha/beta/rc tags 31 | release = error_tracker.__version__ 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.autodoc', 44 | 'sphinx.ext.intersphinx', 45 | 'sphinx.ext.mathjax', 46 | 'sphinx.ext.ifconfig', 47 | 'sphinx.ext.viewcode', 48 | 'sphinx.ext.githubpages', 49 | ] 50 | 51 | # Add any paths that contain templates here, relative to this directory. 52 | templates_path = ['_templates'] 53 | 54 | # The suffix(es) of source filenames. 55 | # You can specify multiple suffix as a list of string: 56 | # 57 | # source_suffix = ['.rst', '.md'] 58 | source_suffix = '.rst' 59 | 60 | # The master toctree document. 61 | master_doc = 'index' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = None 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | # This pattern also affects html_static_path and html_extra_path . 73 | exclude_patterns = [] 74 | 75 | # The name of the Pygments (syntax highlighting) style to use. 76 | pygments_style = 'sphinx' 77 | 78 | # -- Options for HTML output ------------------------------------------------- 79 | 80 | # The theme to use for HTML and HTML Help pages. See the documentation for 81 | # a list of builtin themes. 82 | # 83 | html_theme = 'sphinx_rtd_theme' 84 | 85 | # Theme options are theme-specific and customize the look and feel of a theme 86 | # further. For a list of options available for each theme, see the 87 | # documentation. 88 | # 89 | # html_theme_options = {} 90 | 91 | # Add any paths that contain custom static files (such as style sheets) here, 92 | # relative to this directory. They are copied after the builtin static files, 93 | # so a file named "default.css" will overwrite the builtin "default.css". 94 | html_static_path = ['_static'] 95 | 96 | # Custom sidebar templates, must be a dictionary that maps document names 97 | # to template names. 98 | # 99 | # The default sidebars (for documents that don't match any pattern) are 100 | # defined by theme itself. Builtin themes are using these templates by 101 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 102 | # 'searchbox.html']``. 103 | # 104 | # html_sidebars = {} 105 | 106 | 107 | # -- Options for HTMLHelp output --------------------------------------------- 108 | 109 | # Output file base name for HTML help builder. 110 | htmlhelp_basename = 'error-tracker-doc' 111 | 112 | # -- Options for LaTeX output ------------------------------------------------ 113 | 114 | latex_elements = { 115 | # The paper size ('letterpaper' or 'a4paper'). 116 | # 117 | # 'papersize': 'letterpaper', 118 | 119 | # The font size ('10pt', '11pt' or '12pt'). 120 | # 121 | # 'pointsize': '10pt', 122 | 123 | # Additional stuff for the LaTeX preamble. 124 | # 125 | # 'preamble': '', 126 | 127 | # Latex figure (float) alignment 128 | # 129 | # 'figure_align': 'htbp', 130 | } 131 | 132 | # Grouping the document tree into LaTeX files. List of tuples 133 | # (source start file, target name, title, 134 | # author, documentclass [howto, manual, or own class]). 135 | latex_documents = [ 136 | (master_doc, 'error-tracker.tex', u'error-tracker Documentation', 137 | u'Sonu Kumar', 'manual'), 138 | ] 139 | 140 | # -- Options for manual page output ------------------------------------------ 141 | 142 | # One entry per manual page. List of tuples 143 | # (source start file, name, description, authors, manual section). 144 | man_pages = [ 145 | (master_doc, 'error-tracker', u'error-tracker Documentation', 146 | [author], 1) 147 | ] 148 | 149 | # -- Options for Texinfo output ---------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | (master_doc, 'error-tracker', u'error-tracker Documentation', 156 | author, 'error-tracker', 'Error Tracker Modules for Flask, Django and Python', 157 | 'Miscellaneous'), 158 | ] 159 | 160 | # -- Extension configuration ------------------------------------------------- 161 | 162 | # -- Options for intersphinx extension --------------------------------------- 163 | 164 | # Example configuration for intersphinx: refer to the Python standard library. 165 | intersphinx_mapping = {'https://docs.python.org/': None} 166 | -------------------------------------------------------------------------------- /docs/source/context-builder.rst: -------------------------------------------------------------------------------- 1 | Custom Context Builder 2 | ---------------------- 3 | Having more and more context about failure always help in debugging, by default this app captures HTTP headers, URL parameters, any post data. 4 | More data can be included like data-center name, server details and any other, by default these details are not captured. Nonetheless these details can be captured using ContextBuilderMixin. 5 | Error Tracker comes with two type of context builders DefaultFlaskContextBuilder and DefaultDjangoContextBuilder for Flask and Django respectively. 6 | We can either reuse the same context builder or customize them as per our need. 7 | 8 | **Using ContextBuilderMixin** 9 | 10 | .. note:: 11 | 12 | Implement get_context method of ContextBuilderMixin, default context builders capture *request body*, *headers* and URL parameters. 13 | 14 | .. code:: 15 | 16 | from error_tracker import ContextBuilderMixin 17 | class ContextBuilder(ContextBuilderMixin): 18 | def get_context(self, request, masking=None): 19 | # return context dictionary 20 | 21 | Flask App Usage 22 | =============== 23 | 24 | This custom context builder can be supplied as parameter of AppErrorTracker constructor. 25 | 26 | .. code:: 27 | 28 | error_tracker = AppErrorTracker(app=app, db=db, 29 | context_builder=ContextBuilder()) 30 | 31 | Django App Usage 32 | ================ 33 | 34 | Add path of the custom builder class to the settings file, this class should not take any arguments for constructions. 35 | 36 | **settings.py** 37 | 38 | .. code:: 39 | 40 | APP_ERROR_CONTEXT_BUILDER_MODULE = "path to ContextBuilder class" 41 | 42 | -------------------------------------------------------------------------------- /docs/source/datastore.rst: -------------------------------------------------------------------------------- 1 | Using Mongo or other data store 2 | ------------------------------- 3 | Using any data store as easy as implementing all the methods from **ModelMixin** 4 | 5 | 6 | .. code:: 7 | 8 | from error_tracker import ModelMixin 9 | class CustomModel(ModelMixin): 10 | objects = {} 11 | 12 | @classmethod 13 | def delete_entity(cls, rhash): 14 | ... 15 | 16 | @classmethod 17 | def create_or_update_entity(cls, rhash, host, path, method, request_data, exception_name, traceback): 18 | ... 19 | 20 | @classmethod 21 | def get_exceptions_per_page(cls, page_number=1): 22 | ... 23 | 24 | @classmethod 25 | def get_entity(cls, rhash): 26 | ... 27 | 28 | 29 | Flask App Usage 30 | =============== 31 | Create app with the specific model 32 | 33 | .. code:: 34 | 35 | error_tracker = AppErrorTracker(app=app, model=CustomModel) 36 | 37 | Django App Usage 38 | ================ 39 | 40 | Add path to the model in settings file as 41 | 42 | .. code:: 43 | 44 | APP_ERROR_DB_MODEL = core.CustomModel 45 | -------------------------------------------------------------------------------- /docs/source/django-app.rst: -------------------------------------------------------------------------------- 1 | Django App Settings 2 | ------------------- 3 | 4 | Error Tracker fits nicely with Django framework, error tracker can be configured in different ways. 5 | Multiple settings are available, these settings can be configured using settings file. 6 | 7 | Setting details 8 | ~~~~~~~~~~~~~~~ 9 | 10 | - Home page list size, display 10 exceptions per page 11 | 12 | .. code:: 13 | 14 | EXCEPTION_APP_DEFAULT_LIST_SIZE = 10 15 | 16 | - What all sensitive data should be masked 17 | 18 | .. code:: 19 | 20 | APP_ERROR_MASKED_KEY_HAS = ("password", "secret") 21 | 22 | .. note:: 23 | This means any variables whose name have either password or secret would be masked 24 | 25 | - Sensitive data masking value 26 | 27 | .. code:: 28 | 29 | APP_ERROR_MASK_WITH = '*************' 30 | 31 | - Exception email subject prefix 32 | 33 | .. code:: 34 | 35 | APP_ERROR_SUBJECT_PREFIX = get('APP_ERROR_SUBJECT_PREFIX', '') 36 | 37 | - Email sender's email id 38 | .. code:: 39 | 40 | APP_ERROR_EMAIL_SENDER = "server@example.com" 41 | 42 | - Whom email should be sent in the case of failure 43 | 44 | .. code:: 45 | 46 | APP_ERROR_RECIPIENT_EMAIL = ('dev-group1@example.com', 'dev@example.com') 47 | - By default only 500 errors are tracked but HTTP 404, 401 etc can be tracked as well 48 | 49 | .. code:: 50 | 51 | TRACK_ALL_EXCEPTIONS = True 52 | 53 | .. note:: 54 | Below configurations are required path to some class. 55 | 56 | - Custom Masking Module 57 | 58 | .. code:: 59 | 60 | APP_ERROR_MASKING_MODULE = "path to Masking class" 61 | 62 | - Ticketing/Bugging module 63 | 64 | .. code:: 65 | 66 | APP_ERROR_TICKETING_MODULE = "path to Ticketing class" 67 | 68 | .. note:: 69 | Class must not have any constructor arguments 70 | 71 | - Notifier module 72 | 73 | .. code:: 74 | 75 | APP_ERROR_NOTIFICATION_MODULE = "path to Notification class" 76 | 77 | .. note:: 78 | Class must not have any constructor arguments 79 | 80 | - Context Builder module 81 | 82 | .. code:: 83 | 84 | APP_ERROR_CONTEXT_BUILDER_MODULE = "path to ContextBuilder class" 85 | 86 | .. note:: 87 | Class must not have any constructor arguments 88 | 89 | - Custom Model used for exceptions storage 90 | .. code:: 91 | 92 | APP_ERROR_DB_MODEL = "path to Model class" 93 | 94 | .. note:: 95 | Class must implements all abstract methods 96 | 97 | - Exception Listing page permission 98 | By default exception listing is enabled for only admin users. 99 | 100 | 101 | .. code:: 102 | 103 | APP_ERROR_VIEW_PERMISSION = 'permission.ErrorViewPermission' 104 | 105 | .. note:: 106 | Class must not have any constructor parameters and should implement __call__ method. 107 | 108 | 109 | - Admin site support. 110 | By default this is False, it should be used when default model is used, for custom model you should registered yourself. 111 | 112 | 113 | .. code :: 114 | 115 | APP_ERROR_USE_DJANGO_ADMIN_SITE = True 116 | 117 | 118 | 119 | 120 | Manual Exception Tracking 121 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 122 | 123 | Error can be tracked programmatically using `ErrorTracker`'s utility methods available in error_tracker module. 124 | For tracking exception call `capture_exception` method. 125 | 126 | .. code:: 127 | 128 | from error_tracker import capture_exception 129 | 130 | ... 131 | try 132 | ... 133 | catch Exception as e: 134 | capture_exception(request=request, exception=e) 135 | 136 | 137 | A message can be captured using `capture_message` method. 138 | 139 | .. code:: 140 | 141 | from error_tracker import capture_message 142 | 143 | try 144 | ... 145 | catch Exception as e: 146 | capture_message("Something went wrong", request=request, exception=e) 147 | 148 | 149 | 150 | 151 | Decorator based exception recording, record exception as it occurs in a method call. 152 | 153 | .. note:: 154 | Exception will be re-raised so it must be caught in the caller or ignored. 155 | Re-raising of exception can be disabled using `silent=True` parameter 156 | 157 | .. code:: 158 | 159 | from error_tracker import track_exception 160 | 161 | @track_exception 162 | def do_something(): 163 | ... 164 | 165 | So far, you have seen only uses where context is provided upfront using default context builder or some other means. 166 | Sometimes, we need to put context based on the current code path, like add user_id and email in login flow. 167 | ErrorTracker comes with context manager that can be used for such use cases. 168 | 169 | .. code:: 170 | 171 | from error_tracker import configure_scope 172 | 173 | with configure_scope(request=request) as scope: 174 | scope.set_extra("user_id", 1234) 175 | scope.set_extra("email", "example@example.com" 176 | 177 | 178 | In this case whenever exception would be raised, it will capture the exception automatically and these context details would be stored as well. 179 | 180 | 181 | .. code:: 182 | 183 | { 184 | ... 185 | "context" : { 186 | "id" : 1234, 187 | "email" : "example@example.com" 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /docs/source/flask-app.rst: -------------------------------------------------------------------------------- 1 | Flask App Usage 2 | --------------- 3 | 4 | Lazy initialization 5 | ~~~~~~~~~~~~~~~~~~~ 6 | Use error_tracker.init_app method to configure 7 | 8 | .. code:: 9 | 10 | error_tracker = AppErrorTracker() 11 | ... 12 | error_tracker.init_app(app=app, db=db, notifier=notifier) 13 | 14 | 15 | Config details 16 | ~~~~~~~~~~~~~~ 17 | - Enable or disable notification sending feature 18 | .. code:: 19 | 20 | APP_ERROR_SEND_NOTIFICATION = False 21 | 22 | - Email recipient list 23 | .. code:: 24 | 25 | APP_ERROR_RECIPIENT_EMAIL = None 26 | 27 | - Email subject prefix to be used by email sender 28 | .. code:: 29 | 30 | APP_ERROR_SUBJECT_PREFIX = "" 31 | 32 | - Mask value with following string 33 | .. code:: 34 | 35 | APP_ERROR_MASK_WITH = "**************" 36 | 37 | - Masking rule 38 | App can mask all the variables whose lower case name contains one of the configured string 39 | .. code:: 40 | 41 | APP_ERROR_MASKED_KEY_HAS = ("password", "secret") 42 | 43 | Above configuration will mask the variable names like 44 | .. code:: 45 | 46 | password 47 | secret 48 | PassWord 49 | THis_Is_SEcret 50 | 51 | .. note:: 52 | Any variable names whose lower case string contains either *password* or *secret* 53 | 54 | 55 | - Browse link in your service app 56 | List of exceptions can be seen at */dev/error*, but you can have other prefix as well due to some securities or other reasons. 57 | 58 | .. code:: 59 | 60 | APP_ERROR_URL_PREFIX = "/dev/error" 61 | 62 | - Email address used to construct Message object 63 | .. code:: 64 | 65 | APP_ERROR_EMAIL_SENDER = "prod-issue@example.com" 66 | 67 | 68 | Manual Exception Tracking 69 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 70 | Error can be tracked programmatically using AppErrorTracker's capture_exception method. 71 | ErrorTracker provides many ways to capture error. 72 | 73 | Capture Error using `capture_exception` method 74 | `capture_exception` takes another parameter for `additional_context` (dictionary of key value pairs). 75 | This parameter can be used to provide additional details about the failure. 76 | 77 | 78 | .. code:: 79 | 80 | error_tracker = AppErrorTracker(...) 81 | ... 82 | try 83 | ... 84 | catch Exception as e: 85 | error_tracker.capture_exception() 86 | 87 | 88 | A simple Message can be captured using `capture_message` method. 89 | 90 | 91 | .. code:: 92 | 93 | try 94 | ... 95 | catch Exception as e: 96 | error_tracker.capture_message("Something went wrong!") 97 | 98 | 99 | Decorator based exception recording, record exception as it occurs in a method call. 100 | 101 | .. note:: 102 | Exception will be re-raised so it must be caught in the caller or ignored. 103 | Raised exception can be ignored by passing `silent=True`. 104 | Also more context detail can be provided using `additional_context` parameter. 105 | 106 | 107 | .. code:: 108 | 109 | @error_tracker.auto_track_exception 110 | def fun(): 111 | pass 112 | 113 | 114 | So far, you have seen only uses where context is provided upfront using default context builder or some other means. 115 | Sometimes, we need to put context based on the current code path, like add user_id and email in login flow. 116 | ErrorTracker comes with context manager that can be used for such use cases. 117 | 118 | .. code:: 119 | 120 | from error_tracker import flask_scope 121 | 122 | with flask_scope() as scope: 123 | scope.set_extra("user_id", 1234) 124 | scope.set_extra("email", "example@example.com" ) 125 | 126 | 127 | Now error_tracker will automatically capture exception as it will occur. This data will be stored in request_data detail as 128 | 129 | .. code:: 130 | 131 | { 132 | ... 133 | "context" : { 134 | "id" : 1234, 135 | "email" : "example@example.com" 136 | } 137 | } -------------------------------------------------------------------------------- /docs/source/images/admin1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/docs/source/images/admin1.png -------------------------------------------------------------------------------- /docs/source/images/admin2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/docs/source/images/admin2.png -------------------------------------------------------------------------------- /docs/source/images/detail-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/docs/source/images/detail-page.png -------------------------------------------------------------------------------- /docs/source/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/docs/source/images/home.png -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Error Tracker 3 | ============= 4 | 5 | Error Tracker is a python app plugins for Flask and Django, that provides many of the essentials features of system exceptions tracking. 6 | 7 | Features 8 | -------- 9 | - Mask all the variables, including dict keys, HTTP request body which contain *password* and *secret* in their name. 10 | - Recorded exceptions will be visible to the configured path 11 | - Send notification on failures 12 | - Record exceptions with frame data, including local and global variables 13 | - Raise bugs or update ticket in Bug tracking systems. 14 | - Provide customization for notification, context building, ticketing systems and more 15 | 16 | 17 | **Exception Listing** 18 | 19 | .. image:: images/home.png 20 | :alt: Exception listing 21 | 22 | **Detailed Exception** 23 | 24 | .. image:: images/detail-page.png 25 | :alt: Detailed Exception page 26 | 27 | **Admin dashboard** 28 | 29 | .. image:: images/admin1.png 30 | :alt: Admin dashboard 31 | 32 | 33 | .. image:: images/admin2.png 34 | :alt: Admin dashboard 35 | 36 | 37 | 38 | Quick start 39 | =========== 40 | 41 | Installation 42 | ------------ 43 | 44 | To install Error Tracker, open an interactive shell and run: 45 | 46 | .. code:: 47 | 48 | pip install error-tracker 49 | 50 | 51 | Error Tracker can be used with 52 | 53 | * Standalone Python application 54 | * Flask Application 55 | * Django Application 56 | 57 | Using **Error Tracker** as simple as plugging any other module. 58 | 59 | Recording exception/error 60 | ------------------------- 61 | An error/exception can be recorded using decorator or function call. 62 | 63 | - To record the error using decorator, decorate a function with :code:`track_exception` or :code:`auto_track_exception` 64 | - Where as to record error using function call use :code:`capture_exception` function. 65 | - Exception detail can be written to a file, console or logger etc call method :code:`print_exception` 66 | 67 | All the data will be stored in the configured data store and these data will be available at configure URL path. 68 | 69 | 70 | Flask App setup 71 | ------------------- 72 | An instance of :code:`AppErrorTracker` needs to be created and have to be configured with the correct data. 73 | Monitoring feature can be configured either using object based configuration or app-based configuration, 74 | the only important thing here is we should have all the required key configs in the app.config otherwise it will fail. 75 | 76 | .. note:: 77 | Exception listing page is disabled by default. You need to enable that using view_permission parameter. 78 | view_permission function/callable class must return True/False based on the current request detail. 79 | This method would be called as view_permission(request). 80 | 81 | 82 | For object based configuration add 83 | **settings.py** 84 | 85 | .. code:: 86 | 87 | ... 88 | APP_ERROR_SEND_NOTIFICATION = True 89 | APP_ERROR_RECIPIENT_EMAIL = ('example@example.com',) 90 | APP_ERROR_SUBJECT_PREFIX = "Server Error" 91 | APP_ERROR_EMAIL_SENDER = 'user@example.com' 92 | 93 | **app.py** 94 | 95 | .. code:: 96 | 97 | from flask import Flask 98 | from flask_mail import Mail 99 | import settings 100 | from error_tracker import AppErrorTracker, NotificationMixin 101 | from flask_sqlalchemy import SQLAlchemy 102 | ... 103 | app = Flask(__name__) 104 | app.config.from_object(settings) 105 | db = SQLAlchemy(app) 106 | class Notifier(Mail, NotificationMixin): 107 | def notify(self, request, exception, 108 | email_subject=None, 109 | email_body=None, 110 | from_email=None, 111 | recipient_list=None): 112 | message = Message(email_subject, recipient_list, email_body, sender=from_email) 113 | self.send(message) 114 | mailer = Notifier(app=app) 115 | 116 | # enable for all users 117 | class ViewPermission(ViewPermissionMixin): 118 | def __call__(self, request): 119 | return True 120 | 121 | error_tracker = AppErrorTracker(app=app, db=db, notifier=mailer, view_permission=ViewPermission()) 122 | 123 | 124 | .... 125 | 126 | .... 127 | # Record exception when 404 error code is raised 128 | @app.errorhandler(403) 129 | def error_403(e): 130 | error_tracker.capture_exception() 131 | # any custom logic 132 | 133 | # Record error using decorator 134 | @app.errorhandler(500) 135 | @error_tracker.track_exception 136 | def error_500(e): 137 | # some custom logic 138 | .... 139 | 140 | Here, app, db and notifier parameters are optional. Alternatively, you could use the init_app() method. 141 | 142 | If you start this application and navigate to http://localhost:5000/dev/error, you should see an empty page. 143 | 144 | Django App Setup 145 | ---------------- 146 | 147 | We need to update settings.py file as 148 | 149 | - Add app :code:`error_tracker.DjangoErrorTracker` to installed apps list 150 | - Add Middleware :code:`error_tracker.django.middleware.ExceptionTrackerMiddleWare` for exception tracking [1]_. 151 | - Other configs related to notification 152 | - Add URLs to the list of URL patterns 153 | - Enable django admin site (optional). 154 | 155 | .. [1] This should be added at the end so that it can process exception 1st in the middleware call stack. 156 | 157 | .. note:: 158 | Exception listing page is only enable for super users by default. 159 | You can enable for others by providing a custom implementation of ViewPermissionMixin. 160 | This class must return True/False based on the current request, False means not authorized, True means authorized. 161 | 162 | .. code:: 163 | 164 | ... 165 | APP_ERROR_RECIPIENT_EMAIL = ('example@example.com',) 166 | APP_ERROR_SUBJECT_PREFIX = "Server Error" 167 | APP_ERROR_EMAIL_SENDER = 'user@example.com' 168 | # optional setting otherwise it's enabled for super users only 169 | APP_ERROR_VIEW_PERMISSION = 'permission.ErrorViewPermission' 170 | 171 | INSTALLED_APPS = [ 172 | ... 173 | 'error_tracker.DjangoErrorTracker' 174 | ] 175 | MIDDLEWARE = [ 176 | ... 177 | 'error_tracker.django.middleware.ExceptionTrackerMiddleWare' 178 | ] 179 | 180 | 181 | We need to add URLs to the urls.py so that we can browse the default pages provided by Error Tracker 182 | 183 | .. code:: 184 | 185 | from error_tracker.django import urls 186 | 187 | urlpatterns = [ 188 | ... 189 | url("dev/", include(urls)), 190 | ] 191 | 192 | To enable the error tracker in the admin site add this line in your settings. 193 | 194 | .. code:: 195 | 196 | APP_ERROR_USE_DJANGO_ADMIN_SITE = True 197 | 198 | Using With Python App (NO WEB SERVER) 199 | ------------------------------------- 200 | Choose either of the preferred framework, flask or Django and configure the app as per their specifications. 201 | For example, if we want to use Flask then do 202 | 203 | - Flask App 204 | * Create Flask App instance 205 | * Create Error Tracker app instance 206 | * DO NOT call run method of Flask app instance 207 | * To track exception call :code:`capture_exception` method 208 | 209 | - Django App 210 | * Create Django App with settings and all configuration 211 | * Set environment variable **DJANGO_SETTINGS_MODULE** 212 | * call :code:`django.setup()` 213 | * :code:`from error_tracker import error_tracker` 214 | * To track exception do :code:`capture_exception(None, exception)` 215 | 216 | 217 | 218 | .. toctree:: 219 | 220 | Flask App 221 | Django App 222 | Notification Sender 223 | Raise or Update BUG 224 | Masking 225 | Custom Context Provider 226 | Persistence Store 227 | 228 | .. automodule:: 229 | error_tracker -------------------------------------------------------------------------------- /docs/source/mask-rule.rst: -------------------------------------------------------------------------------- 1 | Masking Rule 2 | ------------- 3 | Masking is essential for any system so that sensitive information can't be exposed in plain text form. 4 | Flask error monitor provides masking feature, that can be disabled or enabled. 5 | 6 | - Disable masking rule: set :code:`APP_ERROR_MASKED_KEY_HAS = ()` 7 | - To set other mask rule add following lines 8 | 9 | .. code:: 10 | 11 | #Mask all the variables or dictionary keys which contains from one of the following tuple 12 | APP_ERROR_MASKED_KEY_HAS = ( 'secret', 'card', 'credit', 'pass' ) 13 | #Replace value with `###@@@@@###` 14 | APP_ERROR_MASK_WITH = "###@@@@@###" 15 | 16 | 17 | .. note:: 18 | - Masking is performed for each variable like dict, list, set and all and it's done based on the *variable name* 19 | - Masking is performed on the dictionary key as well as e.g. *ImmutableMultiDict*, *QueryDict* standard dict or any object whose super class is dict. 20 | 21 | **Custom masking rule using MaskingMixin** 22 | 23 | .. note:: 24 | implement __call__ method of MaskingMixin 25 | 26 | .. code:: 27 | 28 | from error_tracker import MaskingMixin 29 | class MyMaskingRule(MaskingMixin): 30 | def __call__(self, key): 31 | # Put any logic 32 | # Do not mask return False, None 33 | # To mask return True, Value 34 | 35 | 36 | 37 | Flask App Usage 38 | =============== 39 | 40 | .. code:: 41 | 42 | error_tracker = AppErrorTracker(app=app, db=db, 43 | masking=MyMaskingRule("#########", ('pass', 'card') ) ) 44 | 45 | 46 | Django App Usage 47 | ================ 48 | 49 | **settings.py** 50 | 51 | .. code:: 52 | 53 | APP_ERROR_MASKING_MODULE="path to MyMaskingRule" 54 | APP_ERROR_MASKED_KEY_HAS = ('pass', 'card') 55 | APP_ERROR_MASKED_WITH = "############" -------------------------------------------------------------------------------- /docs/source/notification.rst: -------------------------------------------------------------------------------- 1 | Notification notify feature 2 | ---------------------------- 3 | Notifications are very useful in the case of failure, in different situations notification can be used to notify users using different channels like Slack, Email etc. 4 | Notification feature can be enabled by providing a *NotificationMixin* object. 5 | 6 | .. code:: 7 | 8 | from error_tracker import NotificationMixin 9 | class Notifier(NotificationMixin): 10 | def notify(self, request, exception, 11 | email_subject=None, 12 | email_body=None, 13 | from_email=None, 14 | recipient_list=None): 15 | # add logic here 16 | 17 | 18 | 19 | Flask App Usage 20 | =============== 21 | 22 | .. code:: 23 | 24 | error_tracker = AppErrorTracker(app=app, db=db, notifier=Notifier()) 25 | 26 | Django App Usage 27 | ================ 28 | **settings.py** 29 | 30 | .. code:: 31 | 32 | APP_ERROR_NOTIFICATION_MODULE = "path to Notifier class" 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/source/ticketing.rst: -------------------------------------------------------------------------------- 1 | Ticketing 2 | --------- 3 | Ticketing interface can be used to create tickets in the systems like Jira, Bugzilla etc, ticketing can be enabled using ticketing interface. 4 | 5 | **Using TicketingMixin class** 6 | 7 | 8 | implement raise_ticket method of TicketingMixin interface 9 | 10 | .. code:: 11 | 12 | from error_tracker import TicketingMixin 13 | class Ticketing(TicketingMixin): 14 | def raise_ticket(self, exception, request=None): 15 | # Put your logic here 16 | 17 | Flask App Usage 18 | =============== 19 | 20 | .. code:: 21 | 22 | app = Flask(__name__) 23 | db = SQLAlchemy(app) 24 | error_tracker = AppErrorTracker(app=app, db=db, ticketing=Ticketing() ) 25 | db.create_all() 26 | 27 | 28 | Django App Usage 29 | ================ 30 | **settings.py** 31 | 32 | .. code:: 33 | 34 | APP_ERROR_TICKETING_MODULE = "path to Ticketing class" 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /error_tracker/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Error Tracking app 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | __version__ = '3.1.1' 10 | __author__ = 'Sonu Kumar' 11 | __email__ = 'sonunitw12@gmail.com' 12 | 13 | from error_tracker.libs.mixins import * 14 | from error_tracker.libs.exception_formatter import * 15 | 16 | flaskInstalled = False 17 | try: 18 | import flask 19 | 20 | flaskInstalled = True 21 | except ImportError: 22 | pass 23 | 24 | if flaskInstalled: 25 | from error_tracker.flask import * 26 | from error_tracker.flask.utils import configure_scope as flask_scope 27 | 28 | djangoInstalled = False 29 | try: 30 | import django 31 | 32 | djangoInstalled = True 33 | except ImportError as e: 34 | pass 35 | 36 | if djangoInstalled: 37 | from error_tracker.django import * 38 | from error_tracker.django.apps import DjangoErrorTracker 39 | from error_tracker.django.utils import capture_message, track_exception, configure_scope, capture_exception 40 | 41 | __all__ = [ 42 | # flask modules 43 | "AppErrorTracker", "DefaultFlaskContextBuilder", "flask_scope", 44 | 45 | # mixin classes 46 | "NotificationMixin", "ModelMixin", "MaskingMixin", 47 | "ContextBuilderMixin", "TicketingMixin", "ViewPermissionMixin", 48 | 49 | # Django modules 50 | "DefaultDjangoContextBuilder", "DjangoErrorTracker", "DefaultDjangoViewPermission", 51 | "capture_message", "track_exception", "configure_scope", "capture_exception", 52 | 53 | # lower level methods 54 | "format_exception", "print_exception" 55 | ] 56 | -------------------------------------------------------------------------------- /error_tracker/django/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django components 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | 10 | from .utils import DefaultDjangoContextBuilder, DjangoNotification, DefaultDjangoViewPermission 11 | from .settings import * 12 | 13 | from .utils import DjangoNotification, DefaultDjangoContextBuilder 14 | from error_tracker.libs.utils import Masking, get_class_from_path, get_class_instance 15 | from error_tracker import ModelMixin, MaskingMixin, TicketingMixin, NotificationMixin, ContextBuilderMixin, \ 16 | ViewPermissionMixin 17 | from django.apps import apps as django_apps 18 | import warnings 19 | 20 | 21 | def get_exception_model(): 22 | """ 23 | Return the APP error model that is active in this project. 24 | """ 25 | from .models import ErrorModel 26 | model_path = APP_ERROR_DB_MODEL 27 | if model_path is None: 28 | warnings.warn("APP_ERROR_DB_MODEL is not set using default model") 29 | return ErrorModel 30 | try: 31 | return django_apps.get_model(model_path, require_ready=False) 32 | except ValueError: 33 | model = get_class_from_path(model_path, ModelMixin, raise_exception=False, 34 | warning_message="Model " + model_path + " is not importable") 35 | if model is not None: 36 | return model 37 | warnings.warn("APP_ERROR_DB_MODEL must be of the form 'app_label.model_name'") 38 | except LookupError: 39 | model = get_class_from_path(model_path, ModelMixin, raise_exception=False, 40 | warning_message="Model " + model_path + " is not importable") 41 | if model is not None: 42 | return model 43 | warnings.warn( 44 | "APP_ERROR_DB_MODEL refers to model '%s' that has not been installed" % model_path 45 | ) 46 | raise LookupError("APP_ERROR_DB_MODEL is set to '%s' but it's not importable" % model_path) 47 | 48 | 49 | def get_masking_module(): 50 | return get_class_instance(APP_ERROR_MASKING_MODULE, MaskingMixin, Masking, 'Masking', APP_ERROR_MASK_WITH, 51 | APP_ERROR_MASKED_KEY_HAS) 52 | 53 | 54 | def get_ticketing_module(): 55 | return get_class_instance(APP_ERROR_TICKETING_MODULE, TicketingMixin, None, 'Ticketing') 56 | 57 | 58 | def get_notification_module(): 59 | if APP_ERROR_RECIPIENT_EMAIL and APP_ERROR_EMAIL_SENDER: 60 | return get_class_instance(APP_ERROR_NOTIFICATION_MODULE, NotificationMixin, DjangoNotification, 61 | "Notification") 62 | 63 | 64 | def get_context_builder(): 65 | return get_class_instance(APP_ERROR_CONTEXT_BUILDER_MODULE, ContextBuilderMixin, 66 | DefaultDjangoContextBuilder, "ContextBuilder") 67 | 68 | 69 | def get_view_permission(): 70 | return get_class_instance(APP_ERROR_VIEW_PERMISSION, ViewPermissionMixin, DefaultDjangoViewPermission, 71 | "ViewPermission") 72 | -------------------------------------------------------------------------------- /error_tracker/django/admin.py: -------------------------------------------------------------------------------- 1 | from .models import ErrorModel 2 | from django.contrib import admin 3 | from .settings import APP_ERROR_USE_DJANGO_ADMIN_SITE 4 | 5 | if APP_ERROR_USE_DJANGO_ADMIN_SITE: 6 | @admin.register(ErrorModel) 7 | class ErrorModelAdmin(admin.ModelAdmin): 8 | def has_add_permission(self, request): 9 | return False 10 | 11 | date_hierarchy = 'last_seen' 12 | list_display = ( 13 | 'host', 14 | 'path', 15 | 'method', 16 | 'exception_name', 17 | 'count', 18 | 'created_on', 19 | 'last_seen', 20 | 'notification_sent', 21 | 'ticket_raised', 22 | ) 23 | list_filter = ( 24 | 'host', 25 | 'notification_sent', 26 | 'ticket_raised', 27 | ) 28 | search_fields = ('host', 'path', 'exception_name',) 29 | change_form_template = 'error_tracker/admin/change_form.html' 30 | 31 | def changeform_view(self, request, object_id=None, form_url='', extra_context=None): 32 | if object_id: 33 | extra_context = { 34 | 'obj': ErrorModel.objects.get(pk=object_id) 35 | } 36 | return super(ErrorModelAdmin, self).changeform_view(request, object_id=object_id, 37 | form_url=form_url, extra_context=extra_context) 38 | -------------------------------------------------------------------------------- /error_tracker/django/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django error tracker app config 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | 10 | from django.apps import AppConfig 11 | from django.utils.translation import gettext_lazy as _ 12 | 13 | 14 | class DjangoErrorTracker(AppConfig): 15 | name = 'error_tracker.django' 16 | label = 'error_tracker' 17 | verbose_name = _("Error Monitoring and Exception Tracking") 18 | -------------------------------------------------------------------------------- /error_tracker/django/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django error tracker middleware responsible for recording exception 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | from error_tracker.django import get_masking_module, get_context_builder, get_ticketing_module, \ 10 | get_exception_model, get_notification_module, APP_ERROR_SUBJECT_PREFIX, APP_ERROR_EMAIL_SENDER, \ 11 | APP_ERROR_RECIPIENT_EMAIL, TRACK_ALL_EXCEPTIONS, APP_ERROR_NOTIFICATION_ONCE, \ 12 | APP_ERROR_TICKET_ONCE 13 | from error_tracker.libs.utils import get_exception_name, get_context_detail, get_notification_subject 14 | 15 | model = get_exception_model() 16 | ticketing = get_ticketing_module() 17 | masking = get_masking_module() 18 | notifier = get_notification_module() 19 | context_builder = get_context_builder() 20 | 21 | 22 | # noinspection PyMethodMayBeStatic 23 | class ErrorTracker(object): 24 | """ 25 | ErrorTracker class, this is responsible for capturing exceptions and 26 | sending notifications and taking other actions, 27 | """ 28 | 29 | @staticmethod 30 | def _send_notification(request, message, exception, error): 31 | """ 32 | Send notification to the list of entities or call the specific methods 33 | :param request: request object 34 | :param message: message having frame details 35 | :param exception: exception that's triggered 36 | :param error: error model object 37 | :return: None 38 | """ 39 | if notifier is None: 40 | return 41 | if request is not None: 42 | method = request.method 43 | url = request.get_full_path() 44 | else: 45 | method = "" 46 | url = "" 47 | subject = get_notification_subject(APP_ERROR_SUBJECT_PREFIX, 48 | method, url, exception) 49 | notifier.notify(request, 50 | error, 51 | email_subject=subject, 52 | email_body=message, 53 | from_email=APP_ERROR_EMAIL_SENDER, 54 | recipient_list=APP_ERROR_RECIPIENT_EMAIL) 55 | 56 | @staticmethod 57 | def _raise_ticket(request, error): 58 | if ticketing is None: 59 | return 60 | ticketing.raise_ticket(request, error) 61 | 62 | @staticmethod 63 | def _post_process(request, frame_str, frames, error): 64 | send_notification = True 65 | raise_ticket = True 66 | 67 | if request is not None: 68 | message = ('URL: %s' % request.path) + '\n\n' 69 | else: 70 | message = "" 71 | message += frame_str 72 | 73 | if APP_ERROR_NOTIFICATION_ONCE is True and error.notification_sent is True: 74 | send_notification = False 75 | 76 | if APP_ERROR_TICKET_ONCE is True and error.ticket_raised is True: 77 | raise_ticket = False 78 | 79 | if send_notification: 80 | ErrorTracker._send_notification(request, message, frames[-1][:-1], error) 81 | if raise_ticket: 82 | ErrorTracker._raise_ticket(request, error) 83 | 84 | def capture_exception(self, request=None, exception=None, additional_context=None): 85 | """ 86 | Record the exception details and do post processing actions. this method can be used to track any exceptions, 87 | even those are being excepted using try/except block. 88 | :param request: request object 89 | :param exception: what type of exception has occurred 90 | :param additional_context: any additional context 91 | :return: None 92 | """ 93 | if request is not None: 94 | path = request.path 95 | host = request.META.get('HTTP_HOST', '') 96 | method = request.method 97 | else: 98 | path = "" 99 | host = "" 100 | method = "" 101 | 102 | ty, frames, frame_str, traceback_str, rhash, request_data = \ 103 | get_context_detail(request, masking, context_builder, 104 | additional_context=additional_context) 105 | error = model.create_or_update_entity(rhash, host, path, method, 106 | str(request_data), 107 | get_exception_name(ty), 108 | traceback_str) 109 | ErrorTracker._post_process(request, frame_str, frames, error) 110 | 111 | 112 | class ExceptionTrackerMiddleWare(ErrorTracker): 113 | """ 114 | Error tracker middleware that's invoked in the case of exception occurs, 115 | this should be placed at the end of Middleware lists 116 | """ 117 | 118 | def __init__(self, get_response): 119 | self.get_response = get_response 120 | 121 | def __call__(self, request): 122 | return self.get_response(request) 123 | 124 | def process_exception(self, request, exception): 125 | if exception is None and not TRACK_ALL_EXCEPTIONS: 126 | return 127 | self.capture_exception(request, exception) 128 | 129 | 130 | # use this object to track errors in the case of custom failures, where try/except is used 131 | error_tracker = ErrorTracker() 132 | -------------------------------------------------------------------------------- /error_tracker/django/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-11-29 07:29 2 | 3 | from django.db import migrations, models 4 | import error_tracker.libs.mixins 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='ErrorModel', 17 | fields=[ 18 | ('hash', models.CharField(max_length=64, primary_key=True, serialize=False)), 19 | ('host', models.CharField(max_length=1024)), 20 | ('path', models.CharField(max_length=4096)), 21 | ('method', models.CharField(max_length=64)), 22 | ('request_data', models.TextField()), 23 | ('exception_name', models.CharField(max_length=256)), 24 | ('traceback', models.TextField()), 25 | ('count', models.IntegerField(default=0)), 26 | ('created_on', models.DateTimeField(auto_now=True)), 27 | ('last_seen', models.DateTimeField(auto_now=True, db_index=True)), 28 | ], 29 | options={ 30 | 'db_table': 'exceptions', 31 | 'swappable': 'APP_ERROR_DB_MODEL', 32 | }, 33 | bases=(models.Model, error_tracker.libs.mixins.ModelMixin), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /error_tracker/django/migrations/0002_auto_20201018_1311.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-18 11:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('error_tracker', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='errormodel', 15 | name='notification_sent', 16 | field=models.BooleanField(default=False), 17 | ), 18 | migrations.AddField( 19 | model_name='errormodel', 20 | name='ticket_raised', 21 | field=models.BooleanField(default=False), 22 | ), 23 | ] -------------------------------------------------------------------------------- /error_tracker/django/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/error_tracker/django/migrations/__init__.py -------------------------------------------------------------------------------- /error_tracker/django/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django error tracker default model 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | from django.core.paginator import Paginator, EmptyPage 9 | from django.db import models 10 | from error_tracker.libs.mixins import ModelMixin 11 | from django.utils.timezone import now 12 | from traceback import print_exc 13 | from .settings import EXCEPTION_APP_DEFAULT_LIST_SIZE 14 | from collections import namedtuple 15 | 16 | Page = namedtuple("Page", "has_next, next_num, has_prev, prev_num, items ") 17 | 18 | 19 | class AbstractErrorModel(models.Model, ModelMixin): 20 | """ 21 | Base Model to track exceptions 22 | """ 23 | hash = models.CharField(max_length=64, primary_key=True) 24 | host = models.CharField(max_length=1024) 25 | path = models.CharField(max_length=4096) 26 | method = models.CharField(max_length=64) 27 | request_data = models.TextField() 28 | exception_name = models.CharField(max_length=256) 29 | traceback = models.TextField() 30 | count = models.IntegerField(default=0) 31 | created_on = models.DateTimeField(auto_now=True) 32 | last_seen = models.DateTimeField(auto_now=True, db_index=True) 33 | notification_sent = models.BooleanField(default=False) 34 | ticket_raised = models.BooleanField(default=False) 35 | 36 | @classmethod 37 | def get_exceptions_per_page(cls, page_number=1, **query): 38 | if query: 39 | if 'page' in query: 40 | page_number = query['page'] 41 | del query['page'] 42 | query = {"{}__icontains".format(k): v for k, v in query.items()} 43 | 44 | if not query: 45 | records = cls.objects.all().order_by('last_seen') 46 | else: 47 | records = cls.objects.filter(**query).order_by('last_seen') 48 | 49 | paginator = Paginator(records, EXCEPTION_APP_DEFAULT_LIST_SIZE) 50 | try: 51 | page = paginator.page(page_number) 52 | return Page(page.has_next(), 53 | page.next_page_number() if page.has_next() else None, 54 | page.has_previous(), 55 | page.previous_page_number() if page.has_previous() else None, 56 | page.object_list) 57 | except EmptyPage: 58 | return Page(False, None, True, paginator.num_pages, []) 59 | 60 | @classmethod 61 | def get_entity(cls, rhash): 62 | return cls.objects.get(hash=rhash) 63 | 64 | @classmethod 65 | def create_or_update_entity(cls, rhash, host, path, method, request_data, exception_name, traceback): 66 | try: 67 | obj, created = cls.objects.get_or_create(hash=rhash) 68 | if created: 69 | obj.host, obj.path, obj.method, obj.request_data, obj.exception_name, obj.traceback = \ 70 | host, path, method, request_data, exception_name, traceback 71 | obj.count = 1 72 | obj.save() 73 | else: 74 | obj.count += 1 75 | obj.last_seen = now() 76 | obj.save(update_fields=['count', 'last_seen']) 77 | 78 | return obj 79 | except Exception: 80 | print_exc() 81 | 82 | @classmethod 83 | def delete_entity(cls, rhash): 84 | return cls.objects.filter(hash=rhash).delete() 85 | 86 | class Meta: 87 | abstract = True 88 | 89 | 90 | class ErrorModel(AbstractErrorModel): 91 | """ 92 | Default Model to track exceptions 93 | """ 94 | 95 | class Meta(AbstractErrorModel.Meta): 96 | swappable = 'APP_ERROR_DB_MODEL' 97 | db_table = 'exceptions' 98 | -------------------------------------------------------------------------------- /error_tracker/django/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django error tracker default settings 4 | # 5 | # :copyright: 2021 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | from django.conf import settings as dj_settings 10 | from django.core.exceptions import ImproperlyConfigured 11 | 12 | 13 | def get(key, default): 14 | try: 15 | return getattr(dj_settings, key, default) 16 | except ImproperlyConfigured: 17 | return default 18 | 19 | 20 | # App's HOME page default page size 21 | EXCEPTION_APP_DEFAULT_LIST_SIZE = get('EXCEPTION_APP_DEFAULT_LIST_SIZE', 20) 22 | # what all sensitive data should be masked, this means any variables whose name have 23 | # either password or secret would be masked 24 | APP_ERROR_MASKED_KEY_HAS = get('APP_ERROR_MASKED_KEY_HAS', ("password", "secret")) 25 | # Sensitive data masking value 26 | APP_ERROR_MASK_WITH = get('APP_ERROR_MASK_WITH', '*************') 27 | # exception email subject prefix 28 | APP_ERROR_SUBJECT_PREFIX = get('APP_ERROR_SUBJECT_PREFIX', '') 29 | # whose email would be used to send email 30 | APP_ERROR_EMAIL_SENDER = get('APP_ERROR_EMAIL_SENDER', None) 31 | # whom email should be send in the case of failure 32 | APP_ERROR_RECIPIENT_EMAIL = get('APP_ERROR_RECIPIENT_EMAIL', None) 33 | # Whether all types of errors should be tracked or not e.g. 404, 401 etc 34 | TRACK_ALL_EXCEPTIONS = get('TRACK_ALL_EXCEPTIONS', False) 35 | # Path to masking module 36 | APP_ERROR_MASKING_MODULE = get('APP_ERROR_MASKING_MODULE', None) 37 | # path to ticketing module, it should not have any constructor parameters 38 | APP_ERROR_TICKETING_MODULE = get('APP_ERROR_TICKETING_MODULE', None) 39 | # path to notifier module, that would send notifications to entities 40 | APP_ERROR_NOTIFICATION_MODULE = get('APP_ERROR_NOTIFICATION_MODULE', None) 41 | # path to custom context builder 42 | APP_ERROR_CONTEXT_BUILDER_MODULE = get('APP_ERROR_CONTEXT_BUILDER_MODULE', None) 43 | # In case of different database model, provide path to that 44 | APP_ERROR_DB_MODEL = get('APP_ERROR_DB_MODEL', None) 45 | # Check error views are visible to others or not 46 | APP_ERROR_VIEW_PERMISSION = get('APP_ERROR_VIEW_PERMISSION', None) 47 | # Send email notification once 48 | APP_ERROR_NOTIFICATION_ONCE = get('APP_ERROR_NOTIFICATION_ONCE', False) 49 | # Raise ticket once 50 | APP_ERROR_TICKET_ONCE = get('APP_ERROR_NOTIFICATION_ONCE', False) 51 | # Use django admin site 52 | APP_ERROR_USE_DJANGO_ADMIN_SITE = get('APP_ERROR_USE_DJANGO_ADMIN_SITE', False) 53 | -------------------------------------------------------------------------------- /error_tracker/django/templates/error_tracker/admin/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | 3 | 4 | {% block content %} 5 | {% include 'error_tracker/detail.html' %} 6 | {% endblock %} -------------------------------------------------------------------------------- /error_tracker/django/templates/error_tracker/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block head_script %} 11 | 16 | 17 | 34 | {% endblock %} 35 | 36 | {{ title }} 37 | 38 |
39 | {% block header_block %} 40 |

41 | {% trans 'Errors Seen' %} 42 |

43 | {% endblock %} 44 | {% block content_block %} 45 | {% endblock %} 46 |
47 | 48 | -------------------------------------------------------------------------------- /error_tracker/django/templates/error_tracker/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'error_tracker/base.html' %} 2 | {% load error_tracker i18n %} 3 | 4 | {% block content_block %} 5 | {% if error %} 6 |

{{ error }}

7 | {% else %} 8 | 9 |
10 |
{%trans 'Method' %}
11 |
{%trans 'Referrer' %}
12 |
13 | {{ obj.method }} 14 |
15 |
16 | {{ obj.host }}{{ obj.path }} 17 |
18 |
19 | 20 |
21 |
22 |
{%trans 'First time seen' %}
23 |
{{ obj.created_on }}
24 |
25 |
26 |
{%trans 'Last seen' %}
27 |
{{ obj.last_seen }}
28 |
29 |
30 |
{%trans 'Occurrences' %}
31 |
{{ obj.count }}
32 |
33 |
34 |
35 |
{%trans 'Request data' %}
36 |
{{obj.request_data|to_pretty}}
37 |
38 |
39 |
{%trans 'Exception detail' %}
40 |
 {{obj.traceback|escape|replace_new_line_with_br|safe}}
41 |
42 |
43 | 44 | 45 | {% endif %} 46 | {% endblock %} -------------------------------------------------------------------------------- /error_tracker/django/templates/error_tracker/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'error_tracker/base.html' %} 2 | {% load i18n %} 3 | 4 | {% block head_script %} 5 | {{block.super}} 6 | 64 | {%endblock%} 65 | 66 | {% block content_block %} 67 |
68 | {% if error %} 69 |

{{ error }}

70 | {% else %} 71 |
72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {% include 'error_tracker/partials/partial_table.html' %} 94 | 95 | 96 |
{% trans 'Host' %}{% trans 'Method' %}{% trans 'Path' %}{% trans 'Exception' %}{% trans 'Last seen' %}#{% trans 'Action' %}
{% trans 'Reset' %}
97 |
98 | 99 | {% if prev_url or next_url %} 100 | 103 | {% endif %} 104 |
105 | {% endif %} 106 |
107 | {% endblock %} -------------------------------------------------------------------------------- /error_tracker/django/templates/error_tracker/partials/navigation.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% if prev_url %} 4 | 5 | {% trans 'Newer exceptions' %} 6 | {% endif %} 7 | {% if next_url %} 8 | 9 | {% trans 'Older exceptions' %} 10 | 11 | {% endif %} 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /error_tracker/django/templates/error_tracker/partials/partial_table.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% for error in errors.items %} 3 | 4 | {{ error.host }} 5 | {{ error.method }} 6 | 7 | 8 | {{ error.path|truncatechars:30 }} 9 | 10 | 11 | {{ error.exception_name }} 12 | {{ error.last_seen }} 13 | {{ error.count }} 14 | 15 | {%trans 'Delete' %} 17 | 18 | 19 | {% endfor %} 20 | -------------------------------------------------------------------------------- /error_tracker/django/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/error_tracker/django/templatetags/__init__.py -------------------------------------------------------------------------------- /error_tracker/django/templatetags/error_tracker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django error tracker template tags and filters 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | from django import template 10 | import json 11 | from django.utils.safestring import mark_safe 12 | 13 | register = template.Library() 14 | 15 | 16 | @register.filter 17 | def replace_new_line_with_br(value): 18 | return value.replace("\n", "
") 19 | 20 | 21 | @register.filter("to_pretty") 22 | def to_pretty(x): 23 | html = x 24 | try: 25 | x = json.loads(x) 26 | except Exception as e: 27 | try: 28 | x = x.replace("'", '"').replace("\\\\", "\\") 29 | x = json.loads(x) 30 | except Exception as e: 31 | pass 32 | pass 33 | 34 | try: 35 | html = "
{}
".format( 36 | json.dumps(x, indent=4, sort_keys=True)) 37 | except Exception as e: 38 | pass 39 | 40 | return mark_safe(html) 41 | -------------------------------------------------------------------------------- /error_tracker/django/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django error tracker default urls 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | from django.urls import path 10 | from .views import detail, view_list, delete_exception 11 | 12 | app_name = 'error_tracker' 13 | urlpatterns = [ 14 | path('', view_list, name="view_errors"), 15 | path('/delete', delete_exception, name='delete_error'), 16 | path('', detail, name='view_error'), 17 | ] 18 | -------------------------------------------------------------------------------- /error_tracker/django/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django error tracker utils classes 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import json 10 | import re 11 | 12 | from error_tracker.libs.mixins import ContextBuilderMixin, NotificationMixin, ViewPermissionMixin 13 | from error_tracker.libs.utils import get_context_dict 14 | 15 | from django.core.mail import send_mail 16 | from django.http import RawPostDataException 17 | 18 | 19 | class DefaultDjangoContextBuilder(ContextBuilderMixin): 20 | """ 21 | Default request builder, this records, form data, header and URL parameters and mask them if necessary 22 | """ 23 | 24 | @staticmethod 25 | def _get_form_data(request): 26 | form = {} 27 | if request is None: 28 | return form 29 | post = request.POST 30 | if post is None or len(post) == 0: 31 | body = None 32 | try: 33 | body = request.data 34 | except AttributeError: 35 | try: 36 | body = request.body 37 | except RawPostDataException: 38 | pass 39 | if body is not None: 40 | try: 41 | from rest_framework.request import Request 42 | if isinstance(body, Request): 43 | form = body.data 44 | except ImportError: 45 | pass 46 | if len(form) == 0 and len(body) > 0: 47 | try: 48 | form = json.loads(body, encoding="UTF-8") 49 | except Exception: 50 | form = {'data': body} 51 | else: 52 | form = post.dict() 53 | return form 54 | 55 | @staticmethod 56 | def _get_headers(request): 57 | return _HeaderExtractor(request).get_headers() 58 | 59 | @staticmethod 60 | def _get_args(request): 61 | if request is not None: 62 | return request.GET.dict() 63 | 64 | def get_context(self, request, masking=None, additional_context=None): 65 | return str(get_context_dict(headers=self._get_headers(request), 66 | form=self._get_form_data(request), 67 | args=self._get_args(request), 68 | context=additional_context, 69 | masking=masking)) 70 | 71 | 72 | class DjangoNotification(NotificationMixin): 73 | """ 74 | Send emails to the configured users 75 | """ 76 | 77 | def notify(self, request, exception, 78 | email_subject=None, 79 | email_body=None, 80 | from_email=None, 81 | recipient_list=None): 82 | if recipient_list is not None and from_email is not None: 83 | send_mail(email_subject, email_body, from_email, recipient_list, fail_silently=True) 84 | exception.notification_sent = True 85 | exception.save() 86 | 87 | 88 | class DefaultDjangoViewPermission(ViewPermissionMixin): 89 | 90 | def __call__(self, request): 91 | if hasattr(request.user, "is_superuser"): 92 | return request.user.is_superuser 93 | return False 94 | 95 | 96 | class configure_scope(object): 97 | """ 98 | Use this class to work with context manager where more context can be added on the fly 99 | 100 | usage 101 | with configure_scope(request=request) as scope: 102 | scope.set_extra("id", 1234) 103 | 104 | """ 105 | 106 | def __init__(self, request=None, handle_exception=True, context=None): 107 | """ 108 | :param request: current request object 109 | :param handle_exception: whether exception has to be re-raised or not 110 | :param context: initial context detail 111 | """ 112 | self.context = context or {} 113 | self.request = request 114 | self.handle_exception = handle_exception 115 | 116 | def set_extra(self, key, value): 117 | """ 118 | Add key value in context detail 119 | :param key: context key 120 | :param value: context value 121 | :return: 122 | """ 123 | self.context[key] = value 124 | 125 | def __enter__(self): 126 | return self 127 | 128 | def __exit__(self, exc_type, exc_val, exc_tb): 129 | if exc_type is not None: 130 | capture_exception(request=self.request, 131 | exception=exc_type, 132 | additional_context=self.context) 133 | return self.handle_exception 134 | 135 | 136 | def capture_message(message, exception=None, request=None): 137 | """ 138 | Use this method to capture any message once exception is excepted 139 | :param message: message to be recorded 140 | :param exception: exception occurred 141 | :param request: current request object 142 | :return: None 143 | """ 144 | return capture_exception(request=request, exception=exception, 145 | additional_context={'message': message}) 146 | 147 | 148 | def track_exception(func, additional_context=None, request=None, silent=False): 149 | """ 150 | Decorator to be used for automatic exception capture 151 | :param func: function on which it has been used 152 | :param additional_context: additional context detail 153 | :param request: current request 154 | :param silent: exception should be re-raised or not 155 | :return: None 156 | """ 157 | 158 | def wrapper(*args, **kwargs): 159 | try: 160 | return func(*args, **kwargs) 161 | except Exception as ex: 162 | capture_exception(request=request, exception=ex, additional_context=additional_context) 163 | if not silent: 164 | raise ex 165 | 166 | return wrapper 167 | 168 | 169 | def capture_exception(request=None, exception=None, additional_context=None): 170 | """ 171 | Use this method to capture any exception after it has been captured using try except 172 | :param exception: exception occurred 173 | :param request: current request object 174 | :param additional_context: any additional context detail 175 | :return: None 176 | """ 177 | from error_tracker.django.middleware import error_tracker 178 | error_tracker.capture_exception(request=request, exception=exception, 179 | additional_context=additional_context) 180 | 181 | 182 | def clean_value(x): 183 | x = x.value.replace('[["', "").replace('"]]', "").replace('"', "") 184 | return x 185 | 186 | 187 | class _HeaderExtractor(object): 188 | ignored_keys = frozenset(['sec_ch_ua']) 189 | regex = re.compile('^HTTP_') 190 | 191 | def __init__(self, request): 192 | self.request = request 193 | 194 | def _get_raw_headers(self): 195 | if self.request is None: 196 | return {} 197 | try: 198 | return self.request.headers 199 | except AttributeError: 200 | return dict((self.regex.sub('', header), value) for (header, value) 201 | in self.request.META.items() if header.startswith('HTTP_')) 202 | 203 | def _can_be_skipped(self, header_name, header_value): 204 | return header_name.lower() in self.ignored_keys 205 | 206 | def get_headers(self): 207 | headers = self._get_raw_headers() 208 | new_headers = {} 209 | for key, value in headers.items(): 210 | if self._can_be_skipped(key, value): 211 | continue 212 | new_headers[key] = self.get_value(key, value) 213 | return new_headers 214 | 215 | @staticmethod 216 | def get_value(key, value): 217 | try: 218 | # Parse each key, value from headers items and Test if could be "json loaded". 219 | # If not, we set the correspondent value to empty except for cookie key. 220 | json.loads('{"%s":"%s"}' % (key, value)) 221 | except Exception as e: 222 | if key.lower() == "cookie": 223 | try: 224 | from http.cookies import SimpleCookie 225 | try: 226 | cookie = SimpleCookie() 227 | cookie.load(value) 228 | value = {k: clean_value(v) for k, v in cookie.items()} 229 | except Exception as e: 230 | value = "" 231 | except ImportError: 232 | pass 233 | else: 234 | value = "" 235 | return value 236 | -------------------------------------------------------------------------------- /error_tracker/django/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django error tracker default value 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | from django.http import Http404, HttpResponse, JsonResponse 9 | from django.shortcuts import redirect, render 10 | from django.urls import reverse 11 | from django.views.decorators.http import require_GET 12 | from error_tracker.django import get_exception_model, get_view_permission 13 | from django.contrib.auth.decorators import login_required 14 | from django.template.loader import render_to_string 15 | 16 | model = get_exception_model() 17 | 18 | view_permission = get_view_permission() 19 | 20 | 21 | def has_view_permission(func): 22 | def wrapper(request, *args, **kwargs): 23 | if view_permission(request): 24 | return func(request, *args, **kwargs) 25 | return HttpResponse(status=401) 26 | 27 | return wrapper 28 | 29 | 30 | @require_GET 31 | @has_view_permission 32 | def view_list(request): 33 | """ 34 | Home page that lists mose recent exceptions 35 | :param request: request object 36 | :return: rendered template 37 | """ 38 | title = "App Error" 39 | 40 | query = request.GET.dict() 41 | 42 | error = False 43 | errors = model.get_exceptions_per_page(**query) 44 | 45 | next_url = reverse('error_tracker:view_errors') + "?page=" + str(errors.next_num) \ 46 | if errors.has_next else None 47 | 48 | prev_url = reverse('error_tracker:view_errors') + "?page=" + str(errors.prev_num) \ 49 | if errors.has_prev else None 50 | 51 | is_ajax = request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' 52 | if is_ajax or request.GET.get('ajax_partial'): 53 | table = render_to_string('error_tracker/partials/partial_table.html', { 54 | 'errors': errors, 55 | }) 56 | 57 | navigation = render_to_string('error_tracker/partials/navigation.html', { 58 | 'next_url': next_url, 59 | 'prev_url': prev_url 60 | }) 61 | 62 | return JsonResponse({'table': table, 'navigation': navigation}) 63 | 64 | return render(request, template_name='error_tracker/list.html', 65 | context=dict(error=error, title=title, errors=errors, 66 | next_url=next_url, prev_url=prev_url)) 67 | 68 | 69 | @require_GET 70 | @has_view_permission 71 | def delete_exception(request, rhash): 72 | """ 73 | Delete an exceptions 74 | :param request: request object 75 | :param rhash: hash key of the exception 76 | :return: redirect back to home page 77 | """ 78 | model.delete_entity(rhash) 79 | return redirect(reverse('error_tracker:view_errors')) 80 | 81 | 82 | @require_GET 83 | @has_view_permission 84 | def detail(request, rhash): 85 | """ 86 | Display a specific page of the exception 87 | :param request: request object 88 | :param rhash: hash key of the exception 89 | :return: detailed view 90 | """ 91 | obj = model.get_entity(rhash) 92 | error = False 93 | if obj is None: 94 | raise Http404 95 | title = "%s : %s" % (obj.method, obj.path) 96 | return render(request, template_name='error_tracker/detail.html', 97 | context=dict(error=error, title=title, obj=obj)) 98 | -------------------------------------------------------------------------------- /error_tracker/flask/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask components 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | 10 | from .flask_error import AppErrorTracker 11 | from .utils import DefaultFlaskContextBuilder 12 | 13 | __all__ = ["AppErrorTracker", "DefaultFlaskContextBuilder"] 14 | -------------------------------------------------------------------------------- /error_tracker/flask/defaults.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Error tracker flasks plugins default value 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | # Whether notification should be send or not 10 | APP_ERROR_SEND_NOTIFICATION = False 11 | # List of email recipients, these users would be send an email 12 | APP_ERROR_RECIPIENT_EMAIL = None 13 | # Error email subject prefix 14 | APP_ERROR_SUBJECT_PREFIX = "" 15 | # Sensitive data masking value 16 | APP_ERROR_MASK_WITH = "**************" 17 | # what all sensitive data should be masked, this means any variables whose name have 18 | # either password or secret would be masked 19 | APP_ERROR_MASKED_KEY_HAS = ("password", "secret") 20 | # APP URL prefix where endpoint would be exposed 21 | APP_ERROR_URL_PREFIX = "/dev/error" 22 | # Email sender user email's ID 23 | APP_ERROR_EMAIL_SENDER = None 24 | # In Exceptions listing page, number of entry should be displayed 25 | APP_DEFAULT_LIST_SIZE = 20 26 | -------------------------------------------------------------------------------- /error_tracker/flask/templates/error_tracker/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ title }} 11 | 12 |
13 | {% block content_block %} 14 | {% endblock %} 15 |
16 | 17 | -------------------------------------------------------------------------------- /error_tracker/flask/templates/error_tracker/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'error_tracker/base.html' %} 2 | {% block content_block %} 3 | 13 | {% if error %} 14 |

{{ error }}

15 | {% else %} 16 |

Errors Seen

17 |
18 |

URL:{{ obj.host[:-1] }}{{ obj.path }}

19 |
20 |

Method: {{ obj.method }}

21 |
22 |

First time seen: {{ obj.created_on }}

23 |
24 |

Last seen: {{ obj.last_seen }}

25 |
26 |

Occurrences: {{ obj.count }}

27 |
28 |
Request data: {{ obj.request_data }}
29 |
30 |
Exception detail:
31 | {{ obj.traceback.replace('<', "<").replace('>', ">").replace("\n","
" )|safe}}
32 |
33 | {% endif %} 34 | {% endblock %} -------------------------------------------------------------------------------- /error_tracker/flask/templates/error_tracker/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'error_tracker/base.html' %} 2 | {% block content_block %} 3 | {% if error %} 4 |

{{ error }}

5 | {% else %} 6 |

Errors Seen

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for error in errors.items %} 21 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 40 | 41 | {% endfor %} 42 | 43 |
HostMethodPathExceptionLast seenOccurrencesAction
{{ error.host }}{{ error.method }} 25 | 27 | {{ error.path[:30] }} 28 | {% if error.path.__len__() > 30 %} 29 | ... 30 | {% endif %} 31 | 32 | {{ error.exception_name }}{{ error.last_seen }}{{ error.count }} 37 | Delete 39 |
44 | {% if prev_url or next_url %} 45 |
46 |
47 |
48 | {% if prev_url %} 49 | 50 | Newer exceptions 51 | 52 | {% endif %} 53 | {% if next_url %} 54 | 55 | Older exceptions 56 | 57 | {% endif %} 58 |
59 |
60 | {% endif %} 61 | {% endif %} 62 | {% endblock %} -------------------------------------------------------------------------------- /error_tracker/flask/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Utils modules for flask plugin 3 | # 4 | # :copyright: 2023 Sonu Kumar 5 | # :license: BSD-3-Clause 6 | # 7 | from error_tracker import ContextBuilderMixin, ViewPermissionMixin 8 | 9 | 10 | class DefaultFlaskContextBuilder(ContextBuilderMixin): 11 | """ 12 | Default request builder, this records, form data, header and URL parameters and mask them if necessary 13 | """ 14 | 15 | def get_context(self, request, masking=None, additional_context=None): 16 | request_data = dict() 17 | if additional_context is not None and len(additional_context) != 0: 18 | request_data['context'] = additional_context 19 | 20 | if request is not None: 21 | form = dict(request.form) 22 | headers = dict(request.headers) 23 | if masking: 24 | for key in form: 25 | masked, value = masking(key) 26 | if masked: 27 | form[key] = value 28 | for key in headers: 29 | masked, value = masking(key) 30 | if masked: 31 | headers[key] = value 32 | request_data.update({ 33 | 'headers': headers, 34 | 'args': dict(request.args), 35 | 'form': form 36 | }) 37 | return str(request_data) 38 | 39 | 40 | class DefaultFlaskViewPermission(ViewPermissionMixin): 41 | 42 | def __call__(self, request): 43 | return False 44 | 45 | 46 | class configure_scope(object): 47 | """ 48 | Use this class to work with context manager where more context can be added on the fly 49 | 50 | usage 51 | with configure_scope() as scope: 52 | scope.set_extra("id", 1234) 53 | """ 54 | 55 | def __init__(self, error_manager, context=None, handle_exception=True): 56 | """ 57 | Initialize class with error_manager instance. 58 | :param error_manager: AppErrorTracker instance 59 | :param handle_exception: whether raised exception is ignored or not, post exception capture 60 | :param context: initial context detail, dictionary of key value pairs 61 | """ 62 | self.context = context or {} 63 | self.error_manager = error_manager 64 | self.handle_exception = handle_exception 65 | 66 | def set_extra(self, key, value): 67 | self.context[key] = value 68 | 69 | def __enter__(self): 70 | return self 71 | 72 | def __exit__(self, exc_type, exc_val, exc_tb): 73 | if exc_type is not None: 74 | self.error_manager.capture_exception(additional_context=self.context) 75 | return self.handle_exception 76 | -------------------------------------------------------------------------------- /error_tracker/flask/view.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Exception formatter defaults view for flask app 3 | # 4 | # :copyright: 2023 Sonu Kumar 5 | # :license: BSD-3-Clause 6 | # 7 | 8 | import os 9 | 10 | from flask import url_for, render_template, abort, redirect, request, blueprints 11 | 12 | root_path = os.path.abspath(os.path.dirname(__file__)) 13 | 14 | 15 | class Views(object): 16 | def __init__(self, app, model, url_prefix, view_permission): 17 | self.model = model 18 | self.view_permission = view_permission 19 | blueprint = blueprints.Blueprint("app_error", 'app_error', 20 | root_path=root_path, 21 | template_folder="templates", url_prefix=url_prefix) 22 | 23 | @blueprint.route('/') 24 | def view_list(): 25 | """ 26 | List exceptions based on the page number 27 | :return: rendered template 28 | """ 29 | if not view_permission(request): 30 | abort(401) 31 | title = "App Error" 32 | page = request.args.get('page', 1, type=int) 33 | error = False 34 | errors = model.get_exceptions_per_page(page_number=page) 35 | next_url = url_for('app_error.view_list', page=errors.next_num) \ 36 | if errors.has_next else None 37 | prev_url = url_for('app_error.view_list', page=errors.prev_num) \ 38 | if errors.has_prev else None 39 | return render_template('error_tracker/list.html', error=error, title=title, errors=errors, 40 | next_url=next_url, prev_url=prev_url) 41 | 42 | @blueprint.route('/') 43 | def view_detail(rhash): 44 | """ 45 | Display a specific exception having hash rhash 46 | :param rhash: hash key of the exception 47 | :return: detailed view page 48 | """ 49 | if not view_permission(request): 50 | abort(401) 51 | obj = model.get_entity(rhash) 52 | error = False 53 | if obj is None: 54 | abort(404) 55 | title = "%s : %s" % (obj.method, obj.path) 56 | 57 | return render_template('error_tracker/detail.html', error=error, title=title, obj=obj) 58 | 59 | @blueprint.route('/delete/') 60 | def view_delete(rhash): 61 | """ 62 | Delete a specific exceptions identified by rhash 63 | :param rhash: hash key of the exception 64 | :return: redirect back to the home page 65 | """ 66 | if not view_permission(request): 67 | abort(401) 68 | model.delete_entity(rhash) 69 | return redirect(url_for('app_error.view_list')) 70 | 71 | app.register_blueprint(blueprint) 72 | -------------------------------------------------------------------------------- /error_tracker/libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/error_tracker/libs/__init__.py -------------------------------------------------------------------------------- /error_tracker/libs/exception_formatter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Exception formatter that captures frame details in string format. 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import six 10 | import itertools 11 | import sys 12 | import traceback 13 | 14 | try: 15 | import builtins 16 | except ImportError: 17 | import __builtin__ as builtins 18 | try: 19 | from StringIO import StringIO 20 | except ImportError: 21 | from io import StringIO 22 | 23 | import re 24 | import types 25 | 26 | 27 | def convert_if_possible(x): 28 | try: 29 | from werkzeug.datastructures import ImmutableMultiDict 30 | if type(x) == ImmutableMultiDict: 31 | return "ImmutableMultiDict({%s})", x.to_dict(flat=False) 32 | except ImportError: 33 | pass 34 | try: 35 | from django.http import QueryDict 36 | if type(x) == QueryDict: 37 | return "QueryDict({%s})", x.dict() 38 | except ImportError: 39 | pass 40 | return None, x 41 | 42 | 43 | def format_frame(x, max_elements, max_string, max_recursion, masking=None): 44 | """ 45 | Return a formatted frame for storing in the database. 46 | :param x: frame key/value pair 47 | :param max_elements: Maximum number of elements to be formatted 48 | :param max_string: Maximum length for string data types in output 49 | :param max_recursion: Maximum recursion depth used in structure like dict 50 | :param masking: Used to mask a key/value 51 | :return: string of formatted value 52 | """ 53 | 54 | def _per_element(i): 55 | return format_frame(i, max_elements, max_string, max_recursion - 1, masking=masking) 56 | 57 | def _per_dict_element(i): 58 | masked = False 59 | val = None 60 | if masking: 61 | masked, val = masking(i[0]) 62 | return "%r : %s" % (i[0], val if masked else _per_element(i[1])) 63 | 64 | def _it_to_string(fmt, it, per_element=_per_element): 65 | if max_recursion <= 0: 66 | return fmt % "..." 67 | 68 | it = iter(it) 69 | 70 | s = ', '.join(per_element(i) for i in itertools.islice(it, max_elements)) 71 | try: 72 | it.__next__() 73 | except AttributeError: 74 | try: 75 | next(it) 76 | except StopIteration: 77 | return fmt % s 78 | except StopIteration: 79 | return fmt % s 80 | 81 | # Add ellipsis indicating truncation. 82 | # Correctly handle the corner case of max_elements == 0. 83 | return fmt % (s + ", ..." if s else "...") 84 | 85 | if x is builtins.__dict__: 86 | return "" 87 | if type(x) == dict: 88 | return _it_to_string("{%s}", sorted(x.items()), per_element=_per_dict_element) 89 | if type(x) == list: 90 | return _it_to_string("[%s]", x) 91 | if type(x) == tuple: 92 | return _it_to_string("(%s)" if len(x) != 1 93 | or max_recursion <= 0 94 | or max_elements <= 0 95 | else "(%s,)", x) 96 | if type(x) == set: 97 | return _it_to_string("set([%s])", sorted(x)) 98 | if type(x) == frozenset: 99 | return _it_to_string("frozenset([%s])", sorted(x)) 100 | if isinstance(x, six.string_types) and max_string < len(x): 101 | return repr(x[:max_string] + "...") 102 | 103 | converted, x = convert_if_possible(x) 104 | if converted is not None: 105 | return _it_to_string(converted, sorted(x.items()), 106 | per_element=_per_dict_element) 107 | try: 108 | if issubclass(x, dict): 109 | x = dict(x) 110 | return _it_to_string("Dict({%s})", sorted(x.items()), 111 | per_element=_per_dict_element) 112 | except TypeError: 113 | pass 114 | return repr(x) 115 | 116 | 117 | def can_be_skipped(key, value): 118 | # Identifiers that are all uppercase are almost always constants. 119 | if re.match('[A-Z0-9_]+$', key): 120 | return True 121 | # dunder functions. 122 | if re.match('__.*__$', key): 123 | return True 124 | if callable(value): 125 | return True 126 | if isinstance(value, types.ModuleType): 127 | return True 128 | return False 129 | 130 | 131 | def format_exception(tb, max_elements=1000, 132 | max_string=10000, max_recursion=100, 133 | masking=None): 134 | """ 135 | :param tb: traceback 136 | :param max_elements: Maximum number of elements to be printed 137 | :param max_string: Max string length in print 138 | :param max_recursion: Recursive printing in case of dict or other items 139 | :param masking: Masking rule for key/value pair 140 | :return: a formatted string 141 | 142 | Walk over all the frames and get the local variables from the frame and format them using format function and 143 | write to the stringIO based file. 144 | """ 145 | stack = [] 146 | t = tb 147 | while t: 148 | stack.append(t.tb_frame) 149 | t = t.tb_next 150 | buf = StringIO() 151 | w = buf.write 152 | # Go through each frame and format them to get final string 153 | for frame in stack: 154 | w('\n File "%s", line %s, in %s\n' % (frame.f_code.co_filename, 155 | frame.f_lineno, 156 | frame.f_code.co_name)) 157 | local_vars = frame.f_locals.items() 158 | local_vars = sorted(local_vars) 159 | for key, value in local_vars: 160 | if can_be_skipped(key, value): 161 | continue 162 | 163 | w(" %20s = " % key) 164 | masked = False 165 | if masking: 166 | masked, val = masking(key) 167 | if masked: 168 | w(val) 169 | if not masked: 170 | try: 171 | formatted_val = format_frame(value, max_elements, max_string, max_recursion, 172 | masking=masking) 173 | w(formatted_val) 174 | except Exception: 175 | exc_class = sys.exc_info()[0] 176 | w("<%s raised while printing value>" % exc_class) 177 | w("\n") 178 | w("\n") 179 | w(''.join(traceback.format_tb(tb))) 180 | op = buf.getvalue() 181 | buf.close() 182 | return op 183 | 184 | 185 | def print_exception(masking=None, file=sys.stderr): 186 | """ 187 | Print traceback in formatted format 188 | :return: None 189 | """ 190 | ty, val, tb = sys.exc_info() 191 | string = format_exception(tb, masking=masking) 192 | file.write(string) 193 | -------------------------------------------------------------------------------- /error_tracker/libs/mixins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Exception formatter mixin classes 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import abc 10 | 11 | 12 | class MaskingMixin(object): 13 | __metaclass__ = abc.ABCMeta 14 | 15 | def __init__(self, mask_with, mask_key_has): 16 | """ 17 | :param mask_with: masked value to be used for a given variable 18 | :param mask_key_has: tuple of strings to be used for checking masking rule 19 | """ 20 | self.mask_key_has = mask_key_has 21 | self.mask_with = mask_with 22 | 23 | @abc.abstractmethod 24 | def __call__(self, key): 25 | raise NotImplementedError 26 | 27 | 28 | class ModelMixin(object): 29 | """ 30 | Base interface for data mode which can be used to store data in MongoDB/MySQL or any other data store. 31 | """ 32 | hash = None 33 | host = None 34 | path = None 35 | method = None 36 | request_data = None 37 | exception_name = None 38 | traceback = None 39 | count = None 40 | created_on = None 41 | last_seen = None 42 | notification_sent = None 43 | ticket_raised = None 44 | 45 | def __str__(self): 46 | return "'%s' '%s' %s" % (self.host, self.path, self.count) 47 | 48 | def __unicode__(self): 49 | return "'%s' '%s' %s" % (self.host, self.path, self.count) 50 | 51 | def __repr__(self): 52 | return "ModelMixin(%s)" % self.__str__() 53 | 54 | @classmethod 55 | @abc.abstractmethod 56 | def delete_entity(cls, rhash): 57 | """ 58 | :param rhash: lookup key 59 | :return: None 60 | """ 61 | raise NotImplementedError 62 | 63 | @classmethod 64 | @abc.abstractmethod 65 | def create_or_update_entity(cls, rhash, host, path, method, request_data, 66 | exception_name, traceback): 67 | """ 68 | :param rhash: Key of the db entry 69 | :param host: App host e.g. example.com 70 | :param path: request path 71 | :param method: request method (GET/POST/PUT etc) 72 | :param request_data: request form data 73 | :param exception_name: exception name 74 | :param traceback: Exception data captured 75 | :return: error model object 76 | """ 77 | raise NotImplementedError 78 | 79 | @classmethod 80 | @abc.abstractmethod 81 | def get_exceptions_per_page(cls, page_number=1, **kwargs): 82 | """ 83 | An object having these properties, 84 | has_next, next_num, has_prev, prev_num and items 85 | on the returned object like SQLAlchemy's Pagination 86 | :return: a paginated object 87 | """ 88 | raise NotImplementedError 89 | 90 | @classmethod 91 | @abc.abstractmethod 92 | def get_entity(cls, rhash): 93 | """ 94 | :param rhash: key for lookup 95 | :return: Single entry of this class 96 | """ 97 | raise NotImplementedError 98 | 99 | 100 | class NotificationMixin(object): 101 | """ 102 | A notification mixin class which can be used to notify any kind of notification. 103 | """ 104 | __metaclass__ = abc.ABCMeta 105 | 106 | def __init__(self, *args, **kwargs): 107 | pass 108 | 109 | @abc.abstractmethod 110 | def notify(self, request, exception, 111 | email_subject=None, 112 | email_body=None, 113 | from_email=None, 114 | recipient_list=None): 115 | """ 116 | :param request: current request context 117 | :param exception : exception model object 118 | :param email_subject: email subject 119 | :param email_body: email message object 120 | :param from_email: email sender as per config 121 | :param recipient_list: list of email ids for notification 122 | :return: None 123 | """ 124 | raise NotImplementedError 125 | 126 | 127 | class ContextBuilderMixin(object): 128 | """ 129 | A context builder mixing that can be used to provide custom context details. 130 | This will provide entire details, that would be used for DB logging 131 | for usage see default context builder DefaultFlaskContextBuilder 132 | """ 133 | 134 | __metaclass__ = abc.ABCMeta 135 | 136 | def __init__(self, *args, **kwargs): 137 | pass 138 | 139 | @abc.abstractmethod 140 | def get_context(self, request, masking=None, 141 | additional_context=None): 142 | raise NotImplementedError 143 | 144 | 145 | class TicketingMixin(object): 146 | """ 147 | A mixing class that would be called with model object to raise the ticket. 148 | It can be used to directly create or update ticket in ticketing system 149 | """ 150 | __metaclass__ = abc.ABCMeta 151 | 152 | def __init__(self, *args, **kwargs): 153 | pass 154 | 155 | @abc.abstractmethod 156 | def raise_ticket(self, exception, request=None): 157 | """ 158 | :param exception: exception model object 159 | :param request: current request 160 | :return: None 161 | """ 162 | raise NotImplementedError 163 | 164 | 165 | class ViewPermissionMixin(object): 166 | __metaclass__ = abc.ABCMeta 167 | 168 | @abc.abstractmethod 169 | def __call__(self, request): 170 | """ 171 | Check whether user has the permission to view the request 172 | :param request: request object 173 | :return: True/False 174 | """ 175 | raise NotImplementedError 176 | -------------------------------------------------------------------------------- /error_tracker/libs/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Exception formatter utils module 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | import sys 9 | import traceback 10 | import warnings 11 | from hashlib import sha256 12 | import six 13 | 14 | from error_tracker.libs.exception_formatter import format_exception 15 | from error_tracker.libs.mixins import MaskingMixin 16 | 17 | 18 | class Masking(MaskingMixin): 19 | """ 20 | A simple function like class used for masking rule. 21 | """ 22 | 23 | def __call__(self, key): 24 | if isinstance(key, six.string_types): 25 | tmp_key = key.lower() 26 | for k in self.mask_key_has: 27 | if k in tmp_key: 28 | return True, "'%s'" % self.mask_with 29 | return False, None 30 | 31 | 32 | class ConfigError(Exception): 33 | """ 34 | A error class which will be raised by the app if it's not configure properly 35 | """ 36 | pass 37 | 38 | 39 | def get_exception_name(e): 40 | return str(e).replace("'>", "").replace(" 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 403 9 | 10 | -------------------------------------------------------------------------------- /examples/flask-sample/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Server Error 6 | 7 | 8 |

Hmmm! Something went wrong

9 | Home 10 | 11 | -------------------------------------------------------------------------------- /examples/flask-sample/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Home 6 | 7 | 8 |

9 | Error Monitor 10 |

11 |
    12 |
  1. Raise Error
  2. 13 |
  3. List Exception
  4. 14 |
15 | 16 | -------------------------------------------------------------------------------- /locale/fr_FR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , 2020. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2020-10-22 15:02+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Thierry BOULOGNE \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: error_tracker/django/apps.py:17 22 | msgid "Error Monitoring and Exception Tracking" 23 | msgstr "" 24 | 25 | #: error_tracker/django/templates/error_tracker/list.html:86 26 | msgid "Host" 27 | msgstr "Hôtesss" 28 | 29 | #: error_tracker/django/templates/error_tracker/list.html:87 30 | msgid "Method" 31 | msgstr "Méthode" 32 | 33 | #: error_tracker/django/templates/error_tracker/list.html:88 34 | msgid "Path" 35 | msgstr "Chemin" 36 | 37 | #: error_tracker/django/templates/error_tracker/list.html:89 38 | msgid "Exception" 39 | msgstr "Erreur" 40 | 41 | #: error_tracker/django/templates/error_tracker/list.html:90 42 | msgid "Last seen" 43 | msgstr "Dernière occurence" 44 | 45 | #: error_tracker/django/templates/error_tracker/list.html:92 46 | msgid "Action" 47 | msgstr "Action" 48 | 49 | #: error_tracker/django/templates/error_tracker/list.html:101 50 | msgid "Reset" 51 | msgstr "Effacer" 52 | 53 | #: error_tracker/django/templates/error_tracker/partials/navigation.html:5 54 | msgid "Newer exceptions" 55 | msgstr "Suivant" 56 | 57 | #: error_tracker/django/templates/error_tracker/partials/navigation.html:9 58 | msgid "Older exceptions" 59 | msgstr "Précédent" 60 | 61 | #: error_tracker/django/templates/error_tracker/partials/partial_table.html:16 62 | msgid "Delete" 63 | msgstr "Supprimer" 64 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-Mail 3 | Flask-SQLAlchemy 4 | pyquery==1.4.1 5 | Django 6 | six 7 | coveralls 8 | djangorestframework -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | for tst in tests/DjangoTest/runners/*.py; do 4 | python "$tst" 5 | done 6 | 7 | python tests/flask-test-runner.py -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Inside of setup.cfg 2 | [metadata] 3 | description-file = README.rst -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from setuptools import setup 4 | 5 | 6 | def fpath(name): 7 | return os.path.join(os.path.dirname(__file__), name) 8 | 9 | 10 | def read(fname): 11 | return open(fpath(fname)).read() 12 | 13 | 14 | def desc(): 15 | info = read('README.rst') 16 | return info 17 | 18 | 19 | file_text = read(fpath('error_tracker/__init__.py')) 20 | 21 | 22 | def grep(attrname): 23 | pattern = r"{0}\W*=\W*'([^']+)'".format(attrname) 24 | strval, = re.findall(pattern, file_text) 25 | return strval 26 | 27 | 28 | setup( 29 | name='error-tracker', 30 | version=grep('__version__'), 31 | url='https://github.com/sonus21/error-tracker/', 32 | license="BSD-3-Clause", 33 | author=grep('__author__'), 34 | author_email=grep('__email__'), 35 | description='Simple and Extensible Error Monitoring/Tracking framework for Python', 36 | keywords=['Flask', 'error-tracker', 'exception-tracking', 'exception-monitoring', "Django"], 37 | long_description=desc(), 38 | long_description_content_type='text/x-rst', 39 | packages=['error_tracker', ], 40 | include_package_data=True, 41 | python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", 42 | zip_safe=False, 43 | platforms='any', 44 | install_requires=[ 45 | "six", 46 | ], 47 | extras_require={ 48 | "Django": ["Django"], 49 | "DRF": ["djangorestframework"], 50 | "Flask": ["Flask", "Flask-SQLAlchemy"], 51 | }, 52 | tests_require=[ 53 | "Flask-Mail", 54 | 'pyquery', 55 | "Django", 56 | "djangorestframework", 57 | "Flask", 58 | "Flask-SQLAlchemy" 59 | ], 60 | classifiers=[ 61 | 'Development Status :: 5 - Production/Stable', 62 | 'Environment :: Web Environment', 63 | 'Intended Audience :: Developers', 64 | "License :: OSI Approved :: BSD License", 65 | 'Operating System :: OS Independent', 66 | 'Programming Language :: Python', 67 | 'Programming Language :: Python :: 2.7', 68 | 'Programming Language :: Python :: 3.5', 69 | 'Programming Language :: Python :: 3.6', 70 | 'Programming Language :: Python :: 3.7', 71 | 'Programming Language :: Python :: 3.8', 72 | 'Programming Language :: Python :: 3.9', 73 | 'Programming Language :: Python :: 3.10', 74 | 'Topic :: Software Development :: Libraries :: Python Modules', 75 | ] 76 | ) 77 | -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/tests/DjangoTest/DjangoTest/__init__.py -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/custom_model_settings.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | 3 | APP_ERROR_DB_MODEL = 'utils.TestErrorModel' 4 | -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/django_rest_framework_settings.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | 3 | INSTALLED_APPS.append("rest_framework") 4 | ROOT_URLCONF = 'DjangoTest.drf_urls' 5 | TRACK_ALL_EXCEPTIONS = True 6 | -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/drf_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from django.contrib import admin 3 | from rest_framework import routers 4 | from error_tracker.django import urls 5 | from core import views 6 | from core.serializers import UserViewSet 7 | 8 | router = routers.DefaultRouter() 9 | router.register(r'users', UserViewSet) 10 | 11 | urlpatterns = [ 12 | path('admin/', admin.site.urls), 13 | path('dev/', include(urls)), 14 | path('', views.index), 15 | path('value-error', views.value_error), 16 | path('post-view', views.post_view), 17 | path('', include(router.urls)), 18 | path('api-auth/', include('rest_framework.urls')) 19 | ] 20 | -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/masking_custom_settings.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | 3 | APP_ERROR_MASKING_MODULE = "tests.django.util.Masking" 4 | APP_ERROR_MASK_WITH = "THIS_IS_MASK" 5 | APP_ERROR_MASKED_KEY_HAS = ('key', 'password', 'secret') 6 | -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/masking_disabled_settings.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | 3 | APP_ERROR_MASKED_KEY_HAS = [] 4 | -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/notification_settings.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | 3 | APP_ERROR_RECIPIENT_EMAIL = ("test@example.com",) 4 | APP_ERROR_EMAIL_SENDER = "sender@example.com" 5 | APP_ERROR_SUBJECT_PREFIX = "Server Error" 6 | -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for TestApp project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = '4c7$xveisfq6d4wl_ch_8nb53z3_tx9oetka%7apx)dy9_sg#$' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | 'core', 39 | 'error_tracker.DjangoErrorTracker', 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | 'error_tracker.django.middleware.ExceptionTrackerMiddleWare' 51 | ] 52 | 53 | ROOT_URLCONF = 'DjangoTest.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'DjangoTest.wsgi.application' 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 75 | 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.sqlite3', 79 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 80 | } 81 | } 82 | 83 | # Password validation 84 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 85 | 86 | AUTH_PASSWORD_VALIDATORS = [ 87 | { 88 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 89 | }, 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 98 | }, 99 | ] 100 | 101 | # Internationalization 102 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 103 | 104 | LANGUAGE_CODE = 'en-us' 105 | 106 | TIME_ZONE = 'UTC' 107 | 108 | USE_I18N = True 109 | 110 | USE_L10N = True 111 | 112 | USE_TZ = True 113 | 114 | # Static files (CSS, JavaScript, Images) 115 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 116 | 117 | STATIC_URL = '/static/' 118 | EXCEPTION_APP_DEFAULT_LIST_SIZE = 5 119 | APP_ERROR_NOTIFICATION_MODULE = "utils.TestNotification" 120 | APP_ERROR_TICKETING_MODULE = "utils.TicketingSystem" 121 | APP_ERROR_VIEW_PERMISSION = "utils.ViewPermission" 122 | -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/urls.py: -------------------------------------------------------------------------------- 1 | """DjangoSample URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.urls import path, include 17 | from django.contrib import admin 18 | from error_tracker.django import urls 19 | from core import views 20 | 21 | urlpatterns = [ 22 | path('admin/', admin.site.urls), 23 | path('dev/', include(urls)), 24 | path('', views.index), 25 | path('value-error', views.value_error), 26 | path('post-view', views.post_view), 27 | ] 28 | -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/view_401_settings.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | 3 | APP_ERROR_VIEW_PERMISSION = None 4 | -------------------------------------------------------------------------------- /tests/DjangoTest/DjangoTest/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for DjangoSample project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoTest.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tests/DjangoTest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/tests/DjangoTest/__init__.py -------------------------------------------------------------------------------- /tests/DjangoTest/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/tests/DjangoTest/core/__init__.py -------------------------------------------------------------------------------- /tests/DjangoTest/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/tests/DjangoTest/core/migrations/__init__.py -------------------------------------------------------------------------------- /tests/DjangoTest/core/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import routers, serializers, viewsets 3 | 4 | 5 | # Serializers define the API representation. 6 | class UserSerializer(serializers.HyperlinkedModelSerializer): 7 | class Meta: 8 | model = User 9 | fields = ['url', 'username', 'email', 'is_staff'] 10 | 11 | 12 | # ViewSets define the view behavior. 13 | class UserViewSet(viewsets.ModelViewSet): 14 | def create(self, request, *args, **kwargs): 15 | raise NotImplementedError("Me") 16 | 17 | queryset = User.objects.all() 18 | serializer_class = UserSerializer 19 | -------------------------------------------------------------------------------- /tests/DjangoTest/core/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def index(request): 5 | return HttpResponse('No Exception!') 6 | 7 | 8 | def value_error(request): 9 | raise ValueError 10 | 11 | 12 | def post_view(request): 13 | form = request.POST 14 | password = "qwerty" 15 | secret = "pass" 16 | key = "key" 17 | foo_secret = "THIS IS SECRET" 18 | test_password_test = "test_password_test" 19 | TestPassWordTest = "TestPassWordTest" 20 | TestSecret = "TESTSECRET" 21 | l = [1, 2, 3, 4] 22 | t = (1, 2, 3, 4) 23 | d = {'test': 100, "1": 1000} 24 | print(form, password, secret, key, d, foo_secret, 25 | TestPassWordTest, test_password_test, TestSecret, l, t, d) 26 | print(d['KeyError']) 27 | return "KeyError" 28 | -------------------------------------------------------------------------------- /tests/DjangoTest/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoTest.django_rest_framework_settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /tests/DjangoTest/runners/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/tests/DjangoTest/runners/__init__.py -------------------------------------------------------------------------------- /tests/DjangoTest/runners/base_test_runner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import django 4 | from django.conf import settings 5 | from django.test.utils import get_runner 6 | 7 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | TESTS_DIR = os.path.abspath(os.path.join(BASE_DIR, os.pardir)) 9 | ROOT_DIR = os.path.abspath(os.path.join(TESTS_DIR, os.pardir)) 10 | sys.path.insert(0, ROOT_DIR) 11 | sys.path.insert(0, TESTS_DIR) 12 | sys.path.insert(0, BASE_DIR) 13 | sys.path.insert(0, os.path.abspath(os.path.join(BASE_DIR, "DjangoTest"))) 14 | sys.path.insert(0, os.path.abspath(os.path.join(BASE_DIR, "tests"))) 15 | -------------------------------------------------------------------------------- /tests/DjangoTest/runners/common_runner.py: -------------------------------------------------------------------------------- 1 | from base_test_runner import * 2 | 3 | os.environ['DJANGO_SETTINGS_MODULE'] = 'DjangoTest.settings' 4 | django.setup() 5 | TestRunner = get_runner(settings) 6 | test_runner = TestRunner() 7 | failures = test_runner.run_tests(["test_basic", 8 | "test_end_point", 9 | "test_mask_rule", 10 | "test_ticketing", 11 | "test_notification_disabled", 12 | 'test_manual_error_tracking']) 13 | sys.exit(bool(failures)) 14 | -------------------------------------------------------------------------------- /tests/DjangoTest/runners/custom_masking_test_runner.py: -------------------------------------------------------------------------------- 1 | from base_test_runner import * 2 | 3 | os.environ['DJANGO_SETTINGS_MODULE'] = 'DjangoTest.masking_custom_settings' 4 | django.setup() 5 | TestRunner = get_runner(settings) 6 | test_runner = TestRunner() 7 | failures = test_runner.run_tests(["test_custom_masking"]) 8 | -------------------------------------------------------------------------------- /tests/DjangoTest/runners/drf_test_runner.py: -------------------------------------------------------------------------------- 1 | from base_test_runner import * 2 | 3 | os.environ['DJANGO_SETTINGS_MODULE'] = 'DjangoTest.django_rest_framework_settings' 4 | django.setup() 5 | TestRunner = get_runner(settings) 6 | test_runner = TestRunner() 7 | failures = test_runner.run_tests(["test_drf"]) 8 | -------------------------------------------------------------------------------- /tests/DjangoTest/runners/masking_disabled_test_runner.py: -------------------------------------------------------------------------------- 1 | from base_test_runner import * 2 | 3 | os.environ['DJANGO_SETTINGS_MODULE'] = 'DjangoTest.masking_disabled_settings' 4 | django.setup() 5 | TestRunner = get_runner(settings) 6 | test_runner = TestRunner() 7 | failures = test_runner.run_tests(["test_no_masking"]) 8 | sys.exit(bool(failures)) 9 | -------------------------------------------------------------------------------- /tests/DjangoTest/runners/model_test_runner.py: -------------------------------------------------------------------------------- 1 | from base_test_runner import * 2 | 3 | os.environ['DJANGO_SETTINGS_MODULE'] = 'DjangoTest.custom_model_settings' 4 | django.setup() 5 | TestRunner = get_runner(settings) 6 | test_runner = TestRunner() 7 | failures = test_runner.run_tests(["test_db_model"]) 8 | sys.exit(bool(failures)) 9 | -------------------------------------------------------------------------------- /tests/DjangoTest/runners/notification_test_runner.py: -------------------------------------------------------------------------------- 1 | from base_test_runner import * 2 | 3 | os.environ['DJANGO_SETTINGS_MODULE'] = 'DjangoTest.notification_settings' 4 | django.setup() 5 | TestRunner = get_runner(settings) 6 | test_runner = TestRunner() 7 | failures = test_runner.run_tests(["test_notification"]) 8 | sys.exit(bool(failures)) 9 | -------------------------------------------------------------------------------- /tests/DjangoTest/runners/view_401_test_runner.py: -------------------------------------------------------------------------------- 1 | from base_test_runner import * 2 | 3 | os.environ['DJANGO_SETTINGS_MODULE'] = 'DjangoTest.view_401_settings' 4 | django.setup() 5 | TestRunner = get_runner(settings) 6 | test_runner = TestRunner() 7 | failures = test_runner.run_tests(["test_401_end_point"]) 8 | sys.exit(bool(failures)) 9 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/tests/DjangoTest/tests/__init__.py -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_401_end_point.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test all end points are working as expected 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # -*- coding: utf-8 -*- 7 | # 8 | # Test view permission feature 9 | # 10 | # :copyright: 2023 Sonu Kumar 11 | # :license: BSD-3-Clause 12 | # 13 | 14 | 15 | import unittest 16 | 17 | from django.test import TestCase 18 | from util import TestBase 19 | 20 | 21 | class View401Test(TestCase, TestBase): 22 | def verify(self, url): 23 | response = self.get(url, follow=True) 24 | self.assertEqual(401, response.status_code) 25 | 26 | def test_list_view(self): 27 | self.verify("/dev") 28 | 29 | def test_detail_view(self): 30 | self.verify('/dev/xyz') 31 | 32 | def test_delete_view(self): 33 | self.verify("/dev/xyz/delete") 34 | 35 | 36 | if __name__ == '__main__': 37 | unittest.main() 38 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Basic test case test, this tests basic part of application 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | import unittest 9 | 10 | from django.test import LiveServerTestCase 11 | from util import TestBase 12 | 13 | 14 | class BasicTests(object): 15 | def no_exception(self): 16 | result = self.client.get("/") 17 | self.assertEqual(u'No Exception!', result.content.decode('utf-8')) 18 | self.assertEqual(self.get_exceptions(), []) 19 | 20 | def value_error(self): 21 | self.get("/value-error") 22 | errors = self.get_exceptions() 23 | self.assertEqual(len(errors), 1) 24 | error = errors[0] 25 | self.assertIsNotNone(error.hash) 26 | self.assertIsNotNone(error.host) 27 | self.assertIsNotNone(error.path) 28 | self.assertIsNotNone(error.method) 29 | self.assertIsNotNone(error.request_data) 30 | self.assertIsNotNone(error.traceback) 31 | self.assertIsNotNone(error.count) 32 | self.assertIsNotNone(error.created_on) 33 | self.assertIsNotNone(error.last_seen) 34 | 35 | self.assertEqual(error.count, 1) 36 | self.assertEqual(error.method, 'GET') 37 | self.assertEqual(error.path, "/value-error") 38 | 39 | self.get('/value-error') 40 | errors = self.get_exceptions() 41 | self.assertEqual(len(errors), 1) 42 | error_new = errors[-1] 43 | 44 | self.assertEqual(error_new.hash, error.hash) 45 | self.assertEqual(error_new.host, error.host) 46 | self.assertEqual(error_new.path, error.path) 47 | self.assertEqual(error_new.method, error.method) 48 | self.assertEqual(error_new.request_data, error.request_data) 49 | self.assertEqual(error_new.traceback, error.traceback) 50 | self.assertNotEqual(error_new.count, error.count) 51 | self.assertEqual(error_new.created_on, error.created_on) 52 | self.assertNotEqual(error_new.last_seen, error.last_seen) 53 | self.assertEqual(error_new.count, 2) 54 | 55 | self.get('/value-error') 56 | errors = self.get_exceptions() 57 | self.assertEqual(len(errors), 1) 58 | error_new = errors[-1] 59 | self.assertEqual(error_new.count, 3) 60 | 61 | def post_method_error(self): 62 | self.post('/post-view') 63 | errors = self.get_exceptions() 64 | self.assertEqual(len(errors), 1) 65 | error = errors[-1] 66 | self.assertIsNotNone(error.hash) 67 | self.assertIsNotNone(error.host) 68 | self.assertIsNotNone(error.path) 69 | self.assertIsNotNone(error.method) 70 | self.assertIsNotNone(error.request_data) 71 | self.assertIsNotNone(error.traceback) 72 | self.assertIsNotNone(error.count) 73 | self.assertIsNotNone(error.created_on) 74 | self.assertIsNotNone(error.last_seen) 75 | self.assertEqual(error.count, 1) 76 | self.assertEqual(error.method, 'POST') 77 | self.assertEqual(error.path, "/post-view") 78 | 79 | 80 | class BasicTestCase(LiveServerTestCase, TestBase, BasicTests): 81 | 82 | def test_no_exception(self): 83 | self.no_exception() 84 | 85 | def test_value_error(self): 86 | self.value_error() 87 | 88 | def test_post_method_error(self): 89 | self.post_method_error() 90 | 91 | 92 | if __name__ == '__main__': 93 | unittest.main() 94 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_custom_masking.py: -------------------------------------------------------------------------------- 1 | import re 2 | from django.test import TestCase 3 | from util import TestBase 4 | from error_tracker.django.settings import APP_ERROR_MASK_WITH 5 | 6 | 7 | class CustomMaskingRule(TestCase, TestBase): 8 | 9 | def test_verify(self): 10 | form_data = dict( 11 | username="username", 12 | password="password") 13 | self.post('/post-view', data=form_data) 14 | errors = self.get_exceptions() 15 | error = errors[0] 16 | re1 = r".*password.* = .*" 17 | re2 = r".*secret.* = .*" 18 | re3 = r".*password.* : .*" 19 | re4 = r".*key.* = .*" 20 | re1 = re.compile(re1, re.IGNORECASE) 21 | re2 = re.compile(re2, re.IGNORECASE) 22 | re3 = re.compile(re3, re.IGNORECASE) 23 | re4 = re.compile(re4, re.IGNORECASE) 24 | exception = error.traceback 25 | matches1 = re1.findall(exception) 26 | matches2 = re2.findall(exception) 27 | matches3 = re3.findall(exception) 28 | matches4 = re4.findall(exception) 29 | 30 | self.assertEqual(len(matches1), 3) 31 | self.assertEqual(len(matches2), 3) 32 | self.assertEqual(len(matches3), 1) 33 | self.assertEqual(len(matches4), 1) 34 | 35 | for match in matches2 + matches1 + matches4: 36 | key, value = match.split(" = ") 37 | self.assertEqual(str(value), "%r" % APP_ERROR_MASK_WITH) 38 | 39 | for match in matches3: 40 | key, value = match.split(",")[0].split(" : ") 41 | self.assertEqual(value, "%r" % APP_ERROR_MASK_WITH) 42 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_db_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test custom model features 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | 10 | import unittest 11 | 12 | from django.test import LiveServerTestCase 13 | 14 | from test_basic import BasicTests 15 | from util import TestBase 16 | 17 | 18 | class CustomModelClassTest(LiveServerTestCase, TestBase, BasicTests): 19 | def test_no_exception(self): 20 | self.no_exception() 21 | 22 | def test_value_error(self): 23 | from error_tracker import get_exception_model 24 | model = get_exception_model() 25 | self.assertEqual("TestErrorModel", model.__name__) 26 | model.delete_all() 27 | 28 | self.value_error() 29 | 30 | def test_post_method_error(self): 31 | self.post_method_error() 32 | 33 | 34 | if __name__ == '__main__': 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_drf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test Django Rest framework related changes 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import unittest 10 | 11 | from django.test import LiveServerTestCase 12 | from util import TestBase 13 | 14 | 15 | class BasicTestCase(LiveServerTestCase, TestBase): 16 | 17 | def test_no_exception(self): 18 | result = self.client.get("/users/") 19 | self.assertEqual(u'[]', result.content.decode('utf-8')) 20 | self.assertEqual(self.get_exceptions(), []) 21 | 22 | def test_create_user(self): 23 | from_data = {'username': 'admin', 'email': 'example@example.com'} 24 | self.post("/users/", data=from_data) 25 | errors = self.get_exceptions() 26 | self.assertEqual(len(errors), 1) 27 | error = errors[0] 28 | self.assertIsNotNone(error.hash) 29 | self.assertIsNotNone(error.host) 30 | self.assertIsNotNone(error.path) 31 | self.assertIsNotNone(error.method) 32 | self.assertIsNotNone(error.request_data) 33 | self.assertIsNotNone(error.traceback) 34 | self.assertIsNotNone(error.count) 35 | self.assertIsNotNone(error.created_on) 36 | self.assertIsNotNone(error.last_seen) 37 | form = eval(error.request_data)['form'] 38 | self.assertEqual(from_data, form) 39 | 40 | 41 | if __name__ == '__main__': 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_end_point.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test all end points are working as expected 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import unittest 10 | 11 | import pyquery 12 | from django.test import TestCase 13 | from util import TestBase 14 | from django.conf import settings 15 | from error_tracker.django.models import ErrorModel 16 | 17 | 18 | class ViewTestCase(TestCase, TestBase): 19 | def test_list_view(self): 20 | self.get('/value-error') 21 | self.post('/post-view') 22 | html = self.get('/dev', follow=True).content 23 | urls = [node.attrib['href'] for node in pyquery.PyQuery(html)('a.view-link, a.home-link, a.delete')] 24 | # 2 links for delete operation and 2 links to navigate and 1 link to home page 25 | self.assertEqual(len(urls), 2 + 3) 26 | 27 | urls = [node.attrib['href'] for node in pyquery.PyQuery(html)('a.view-link')] 28 | self.assertEqual(len(urls), 2) 29 | 30 | def test_detail_view(self): 31 | self.get('/value-error') 32 | html = self.get('/dev', follow=True).content 33 | url = [node.attrib['href'] for node in pyquery.PyQuery(html)('a.view-link')][0] 34 | response = self.get(url).content 35 | row = pyquery.PyQuery(response)('.mb-4') 36 | self.assertEqual(2, len(row)) 37 | divs = pyquery.PyQuery(response)('.row>div') 38 | self.assertEqual(len(divs), 11) 39 | 40 | def test_delete_view(self): 41 | self.get('/value-error') 42 | html = self.get('/dev', follow=True).content 43 | url = [node.attrib['href'] for node in pyquery.PyQuery(html)('.delete')][0] 44 | self.get(url, follow=True) 45 | self.assertEqual(len(self.get_exceptions()), 0) 46 | 47 | def test_pagination(self): 48 | self.get('/value-error') 49 | exception = self.get_exceptions()[0] 50 | hashx = exception.hash 51 | inserted = 0 52 | i = 0 53 | while inserted < 20: 54 | i += 1 55 | idx = str(i) + hashx[2:] 56 | inserted += 1 57 | ErrorModel.create_or_update_entity(idx, exception.host, exception.path, 58 | exception.method, exception.request_data, 59 | exception.exception_name, 60 | exception.traceback) 61 | 62 | response = self.get('/dev', follow=True).content 63 | urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a.view-link, a.delete, a.pagelink, a.home-link')] 64 | self.assertEqual(len(urls), settings.EXCEPTION_APP_DEFAULT_LIST_SIZE * 2 + 2) 65 | self.assertTrue('/dev/?page=2' in urls) 66 | 67 | response = self.get('/dev/?page=2', follow=True).content 68 | urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a.view-link, a.delete, a.pagelink, a.home-link')] 69 | self.assertEqual(len(urls), settings.EXCEPTION_APP_DEFAULT_LIST_SIZE * 2 + 3) 70 | self.assertTrue('/dev/?page=1' in urls) 71 | self.assertTrue('/dev/?page=3' in urls) 72 | 73 | response = self.get('/dev/?page=5', follow=True).content 74 | urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a.view-link, a.delete, a.pagelink, a.home-link')] 75 | self.assertTrue('/dev/?page=4' in urls) 76 | 77 | response = self.get('/dev/?page=6', follow=True).content 78 | urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a.view-link, a.delete, a.pagelink, a.home-link')] 79 | self.assertEqual(len(urls), 2) 80 | 81 | 82 | if __name__ == '__main__': 83 | unittest.main() -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_manual_error_tracking.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Basic test case test, this tests basic part of application 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | import unittest 9 | 10 | from django.test import LiveServerTestCase 11 | from util import TestBase 12 | from error_tracker import track_exception, capture_exception, capture_message, configure_scope 13 | 14 | 15 | class RecordErrorTest(LiveServerTestCase, TestBase): 16 | def throw(self): 17 | password = "qwerty" 18 | secret = "pass" 19 | key = "key" 20 | foo_secret = "THIS IS SECRET" 21 | test_password_test = "test_password_test" 22 | TestPassWordTest = "TestPassWordTest" 23 | TestSecret = "TESTSECRET" 24 | l = [1, 2, 3, 4] 25 | t = (1, 2, 3, 4) 26 | d = {'test': 100, "1": 1000} 27 | print(password, secret, key, d, foo_secret, 28 | TestPassWordTest, test_password_test, TestSecret, l, t, d) 29 | print(d['KeyError']) 30 | return "KeyError" 31 | 32 | def verify(self, data="{}"): 33 | errors = self.get_exceptions() 34 | self.assertEqual(len(errors), 1) 35 | error = errors[-1] 36 | self.assertIsNotNone(error.hash) 37 | self.assertEqual(error.count, 1) 38 | self.assertEqual(error.method, '') 39 | self.assertEqual(error.path, "") 40 | self.assertEqual(error.host, "") 41 | self.assertEqual(error.request_data, data) 42 | self.assertIsNotNone(error.traceback) 43 | self.assertIsNotNone(error.created_on) 44 | self.assertIsNotNone(error.last_seen) 45 | 46 | def test_decorator_recording(self): 47 | @track_exception 48 | def fun(a, b, x=123): 49 | print(a, b, x) 50 | self.throw() 51 | 52 | try: 53 | fun(1, 2, x=456) 54 | except Exception: 55 | pass 56 | self.verify() 57 | 58 | def test_record(self): 59 | try: 60 | self.throw() 61 | except Exception as e: 62 | capture_exception(None, e) 63 | self.verify() 64 | 65 | def test_message(self): 66 | try: 67 | self.throw() 68 | except Exception: 69 | capture_message("Something went wrong.") 70 | self.verify(data="{'context': {'message': 'Something went wrong.'}}") 71 | 72 | def test_additional_context(self): 73 | with configure_scope() as f: 74 | f.set_extra("id", 1234) 75 | self.throw() 76 | self.verify(data="{'context': {'id': 1234}}") 77 | 78 | def test_context_manager_exception_not_handled(self): 79 | try: 80 | with configure_scope(handle_exception=False) as f: 81 | f.set_extra("id", 1234) 82 | self.throw() 83 | self.assertTrue(False) 84 | except Exception: 85 | self.verify(data="{'context': {'id': 1234}}") 86 | pass 87 | 88 | def test_context_manager_with_initial_context(self): 89 | try: 90 | with configure_scope(context={"id": 1234}) as f: 91 | self.throw() 92 | self.assertTrue(False) 93 | except Exception: 94 | self.verify(data="{'context': {'id': 1234}}") 95 | 96 | 97 | if __name__ == '__main__': 98 | unittest.main() 99 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_mask_rule.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Masking rule tests 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import re 10 | import unittest 11 | 12 | from django.test import TestCase 13 | from util import TestBase 14 | from error_tracker.django.settings import APP_ERROR_MASK_WITH 15 | 16 | 17 | class DefaultMaskingRule(TestBase, TestCase): 18 | def test_mask_key(self): 19 | self.post('/post-view') 20 | errors = self.get_exceptions() 21 | error = errors[0] 22 | re1 = r".*password.* = .*" 23 | re2 = r".*secret.* = .*" 24 | re1 = re.compile(re1, re.IGNORECASE) 25 | re2 = re.compile(re2, re.IGNORECASE) 26 | exception = error.traceback 27 | matches1 = re1.findall(exception) 28 | matches2 = re2.findall(exception) 29 | self.assertEqual(len(matches1), 3) 30 | self.assertEqual(len(matches2), 3) 31 | for match in matches2 + matches1: 32 | key, value = match.split(" = ") 33 | self.assertEqual(value, "%r" % APP_ERROR_MASK_WITH) 34 | 35 | def test_mask_key_form(self): 36 | form_data = dict( 37 | username="username", 38 | password="password") 39 | self.post('/post-view', data=form_data) 40 | errors = self.get_exceptions() 41 | error = errors[0] 42 | re1 = r".*password.* = .*" 43 | re2 = r".*secret.* = .*" 44 | re1 = re.compile(re1, re.IGNORECASE) 45 | re2 = re.compile(re2, re.IGNORECASE) 46 | exception = error.traceback 47 | print("EXCEPTION", exception) 48 | matches1 = re1.findall(exception) 49 | matches2 = re2.findall(exception) 50 | self.assertEqual(len(matches1), 3) 51 | self.assertEqual(len(matches2), 3) 52 | for match in matches2 + matches1: 53 | key, value = match.split(" = ") 54 | self.assertEqual(value, "%r" % APP_ERROR_MASK_WITH) 55 | 56 | re3 = r".*password.* : .*" 57 | re3 = re.compile(re3, re.IGNORECASE) 58 | 59 | matches3 = re3.findall(exception) 60 | self.assertEqual(len(matches3), 1) 61 | for match in matches3: 62 | words = match.split("' : ") 63 | key, value = words[0], words[1].split(",")[0] 64 | self.assertEqual(value, u"%r" % APP_ERROR_MASK_WITH) 65 | 66 | 67 | 68 | if __name__ == '__main__': 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_no_masking.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test no masking feature 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import re 10 | 11 | from django.test import TestCase 12 | from util import TestBase 13 | from error_tracker.django.settings import APP_ERROR_MASK_WITH 14 | 15 | 16 | class NoMasking(TestBase, TestCase): 17 | def test_no_mask(self): 18 | self.post('/post-view', data=dict(username="username", password="password")) 19 | errors = self.get_exceptions() 20 | error = errors[0] 21 | re1 = r".*password.* = .*" 22 | re2 = r".*secret.* = .*" 23 | re3 = r".*form.* = .*" 24 | re4 = r'.*l.* = \[.*\]' 25 | re1 = re.compile(re1, re.IGNORECASE) 26 | re2 = re.compile(re2, re.IGNORECASE) 27 | re3 = re.compile(re3, re.IGNORECASE) 28 | re4 = re.compile(re4, re.IGNORECASE) 29 | 30 | exception = error.traceback 31 | matches1 = re1.findall(exception) 32 | matches2 = re2.findall(exception) 33 | self.assertEqual(len(matches1), 3) 34 | self.assertEqual(len(matches2), 3) 35 | for match in matches2 + matches1: 36 | key, value = match.split(" = ") 37 | self.assertNotEqual(value, "%r" % APP_ERROR_MASK_WITH) 38 | 39 | matches3 = re3.findall(exception) 40 | matches4 = re4.findall(exception) 41 | self.assertEqual(1, len(matches4)) 42 | self.assertEqual(1, len(matches3)) 43 | self.assertEqual("[1, 2, 3, 4]", matches4[0].strip().split("l = ")[1]) 44 | data = matches3[0].strip().split("form = ")[1].split("QueryDict(")[1].split(')')[0] 45 | self.assertEqual(True, data == "{'password' : 'password', 'username' : 'username'}") 46 | re3 = r".*password.* : .*" 47 | re3 = re.compile(re3, re.IGNORECASE) 48 | matches3 = re3.findall(exception) 49 | self.assertEqual(len(matches3), 1) 50 | for match in matches3: 51 | words = match.split("' : ") 52 | key, value = words[0], words[1].split(",")[0] 53 | self.assertNotEqual(value, "%r" % APP_ERROR_MASK_WITH) 54 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_notification.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Notification feature tests 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import unittest 10 | from django.test import TestCase 11 | from util import TestBase 12 | from DjangoTest.notification_settings import APP_ERROR_SUBJECT_PREFIX 13 | 14 | 15 | class NotificationConfigurationTests(TestBase, TestCase): 16 | def test_email_send(self): 17 | self.get('/value-error') 18 | notifications = self.get_notifications() 19 | self.assertEqual(len(notifications), 1) 20 | self.assertTrue(APP_ERROR_SUBJECT_PREFIX in notifications[0][0]) 21 | self.get('/value-error') 22 | self.assertEqual(len(self.get_notifications()), 2) 23 | self.post('/value-error') 24 | self.assertEqual(len(self.get_notifications()), 3) 25 | self.clear_notifications() 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_notification_disabled.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Notification feature disabled 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import unittest 10 | from django.test import TestCase 11 | from util import TestBase 12 | from error_tracker.django.middleware import notifier 13 | 14 | 15 | class NotificationDisabledTest(TestBase, TestCase): 16 | def test_email_send(self): 17 | self.assertIsNone(notifier) 18 | 19 | 20 | if __name__ == '__main__': 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/test_ticketing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Ticketing feature test, this tests whether raise_ticket method is called or not. 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | 10 | import unittest 11 | from django.test import LiveServerTestCase 12 | from error_tracker.django.middleware import ticketing 13 | from util import TestBase 14 | 15 | 16 | class TicketingTest(LiveServerTestCase, TestBase): 17 | def test_tickets_are_raise(self): 18 | ticketing.clear() 19 | self.get('/value-error') 20 | self.assertEqual(len(ticketing.get_tickets()), 1) 21 | self.get('/value-error') 22 | self.assertEqual(len(ticketing.get_tickets()), 2) 23 | self.post('/value-error') 24 | self.assertEqual(len(ticketing.get_tickets()), 3) 25 | ticketing.clear() 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /tests/DjangoTest/tests/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test utils 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | from error_tracker.django import get_exception_model 10 | from error_tracker.django.middleware import notifier 11 | 12 | 13 | class TestBase(object): 14 | 15 | def get_exceptions(self): 16 | model = get_exception_model() 17 | return list(model.get_exceptions_per_page().items) 18 | 19 | def create_or_update_exception(self, rhash, host, path, method, request_data, exception_name, traceback): 20 | model = get_exception_model() 21 | return model.create_or_update_entity(rhash, host, path, method, request_data, exception_name, traceback) 22 | 23 | def get_notifications(self): 24 | return notifier.get_notifications() 25 | 26 | def clear_notifications(self): 27 | return notifier.clear() 28 | 29 | def get(self, path, **kwargs): 30 | try: 31 | return self.client.get(path, **kwargs) 32 | except Exception as e: 33 | print(e) 34 | return None 35 | 36 | def post(self, path, **kwargs): 37 | try: 38 | return self.client.post(path, **kwargs) 39 | except Exception as e: 40 | print(e) 41 | return None 42 | -------------------------------------------------------------------------------- /tests/FlaskTest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/tests/FlaskTest/__init__.py -------------------------------------------------------------------------------- /tests/FlaskTest/configs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/tests/FlaskTest/configs/__init__.py -------------------------------------------------------------------------------- /tests/FlaskTest/configs/custom_mask_rule.py: -------------------------------------------------------------------------------- 1 | APP_ERROR_SEND_NOTIFICATION = True 2 | APP_ERROR_RECIPIENT_EMAIL = None 3 | APP_ERROR_SUBJECT_PREFIX = "" 4 | APP_ERROR_MASK_WITH = "THIS_IS_MASK" 5 | APP_ERROR_MASKED_KEY_HAS = ('key', 'password', 'secret') 6 | APP_ERROR_URL_PREFIX = "/dev/error" 7 | -------------------------------------------------------------------------------- /tests/FlaskTest/configs/masking_disabled.py: -------------------------------------------------------------------------------- 1 | APP_ERROR_SEND_NOTIFICATION = True 2 | APP_ERROR_RECIPIENT_EMAIL = None 3 | APP_ERROR_SUBJECT_PREFIX = "" 4 | APP_ERROR_MASK_WITH = "**************" 5 | APP_ERROR_MASKED_KEY_HAS = None 6 | APP_ERROR_URL_PREFIX = "/dev/error" 7 | -------------------------------------------------------------------------------- /tests/FlaskTest/configs/notification_config_disabled.py: -------------------------------------------------------------------------------- 1 | APP_ERROR_SEND_NOTIFICATION = True 2 | APP_ERROR_RECIPIENT_EMAIL = None 3 | APP_ERROR_SUBJECT_PREFIX = "" 4 | APP_ERROR_MASK_WITH = "**************" 5 | APP_ERROR_MASKED_KEY_HAS = ("password", "secret") 6 | APP_ERROR_URL_PREFIX = "/dev/error" 7 | -------------------------------------------------------------------------------- /tests/FlaskTest/configs/notification_config_enabled.py: -------------------------------------------------------------------------------- 1 | APP_ERROR_SEND_NOTIFICATION = True 2 | APP_ERROR_RECIPIENT_EMAIL = ["test@example.com", "test2@example.com"] 3 | APP_ERROR_SUBJECT_PREFIX = "Server Error" 4 | APP_ERROR_MASK_WITH = "**************" 5 | APP_ERROR_MASKED_KEY_HAS = ("password", "secret") 6 | APP_ERROR_URL_PREFIX = "/dev/error" 7 | APP_ERROR_EMAIL_SENDER = "sender@example.com" 8 | -------------------------------------------------------------------------------- /tests/FlaskTest/configs/pagination_config.py: -------------------------------------------------------------------------------- 1 | APP_DEFAULT_LIST_SIZE = 5 2 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_401_views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test view permission feature 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | 10 | import unittest 11 | 12 | from flask import Flask 13 | 14 | from error_tracker import AppErrorTracker 15 | from tests.utils import TestErrorModel 16 | from .utils import BaseTestMixin 17 | 18 | 19 | class View401Test(BaseTestMixin): 20 | db_prefix = "View401" 21 | 22 | def _setup(self, db_name): 23 | app = Flask(__name__) 24 | TestErrorModel.delete_all() 25 | error_tracker = AppErrorTracker(app=app, model=TestErrorModel) 26 | return app, None, error_tracker 27 | 28 | def verify(self, db_name, url): 29 | app, db, _ = self.setUpApp(db_name) 30 | with app.test_client() as c: 31 | response = c.get(url, follow_redirects=True) 32 | self.assertEquals(response.status_code, 401) 33 | 34 | def test_list_view(self): 35 | self.verify("test_list_view", '/dev/error') 36 | 37 | def test_detail_view(self): 38 | self.verify("test_detail_view", '/dev/error/xyz') 39 | 40 | def test_delete_view(self): 41 | self.verify("test_delete_view", '/dev/error/delete/xyz') 42 | 43 | 44 | if __name__ == '__main__': 45 | unittest.main() 46 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Basic test case test, this tests basic part of application 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | import unittest 9 | from .utils import TestCaseMixin 10 | 11 | 12 | class BasicTest(TestCaseMixin): 13 | db_prefix = "BasicTest" 14 | 15 | def test_no_exception(self): 16 | db_file = "test_no_exception" 17 | app, db, error_tracker = self.setUpApp(db_file) 18 | with app.test_client() as c: 19 | result = c.get('/') 20 | self.assertEqual(u'No Exception!', result.data.decode('utf-8')) 21 | self.assertEqual(error_tracker.get_exceptions(), []) 22 | 23 | def test_value_error(self): 24 | db_name = "test_value_error" 25 | app, db, error_tracker = self.setUpApp(db_name) 26 | with app.test_client() as c: 27 | result = c.get('/value-error') 28 | self.assertEqual(u'500', result.data.decode('utf-8')) 29 | errors = error_tracker.get_exceptions() 30 | self.assertEqual(len(errors), 1) 31 | error = errors[0] 32 | self.assertIsNotNone(error.hash) 33 | self.assertIsNotNone(error.host) 34 | self.assertIsNotNone(error.path) 35 | self.assertIsNotNone(error.method) 36 | self.assertIsNotNone(error.request_data) 37 | self.assertIsNotNone(error.traceback) 38 | self.assertIsNotNone(error.count) 39 | self.assertIsNotNone(error.created_on) 40 | self.assertIsNotNone(error.last_seen) 41 | 42 | self.assertEqual(error.count, 1) 43 | self.assertEqual(error.method, 'GET') 44 | self.assertEqual(error.path, "/value-error") 45 | 46 | c.get('/value-error') 47 | errors = error_tracker.get_exceptions() 48 | self.assertEqual(len(errors), 1) 49 | error_new = errors[-1] 50 | 51 | self.assertEqual(error_new.hash, error.hash) 52 | self.assertEqual(error_new.host, error.host) 53 | self.assertEqual(error_new.path, error.path) 54 | self.assertEqual(error_new.method, error.method) 55 | self.assertEqual(error_new.request_data, error.request_data) 56 | self.assertEqual(error_new.traceback, error.traceback) 57 | self.assertNotEqual(error_new.count, error.count) 58 | self.assertEqual(error_new.created_on, error.created_on) 59 | self.assertNotEqual(error_new.last_seen, error.last_seen) 60 | self.assertEqual(error_new.count, 2) 61 | 62 | c.post('/value-error') 63 | errors = error_tracker.get_exceptions() 64 | self.assertEqual(len(errors), 1) 65 | error_new = errors[-1] 66 | self.assertEqual(error_new.count, 3) 67 | 68 | def test_post_method_error(self): 69 | db_name = "post_method_error" 70 | app, db, error_tracker = self.setUpApp(db_name) 71 | with app.test_client() as c: 72 | c.post('/post-view') 73 | errors = error_tracker.get_exceptions() 74 | self.assertEqual(len(errors), 1) 75 | error = errors[-1] 76 | self.assertIsNotNone(error.hash) 77 | self.assertIsNotNone(error.host) 78 | self.assertIsNotNone(error.path) 79 | self.assertIsNotNone(error.method) 80 | self.assertIsNotNone(error.request_data) 81 | self.assertIsNotNone(error.traceback) 82 | self.assertIsNotNone(error.count) 83 | self.assertIsNotNone(error.created_on) 84 | self.assertIsNotNone(error.last_seen) 85 | self.assertEqual(error.count, 1) 86 | self.assertEqual(error.method, 'POST') 87 | self.assertEqual(error.path, "/post-view") 88 | 89 | 90 | if __name__ == '__main__': 91 | unittest.main() 92 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_db_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test custom model features 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | 10 | import unittest 11 | from flask import Flask 12 | from error_tracker import AppErrorTracker 13 | from tests.utils import TestErrorModel 14 | from .test_basic import BasicTest 15 | from tests.utils import ViewPermission 16 | 17 | 18 | class CustomModelClassTest(BasicTest, unittest.TestCase): 19 | db_prefix = "CustomModelClassTest" 20 | 21 | def _setup(self, db_name): 22 | app = Flask(__name__) 23 | TestErrorModel.delete_all() 24 | error_tracker = AppErrorTracker(app=app, model=TestErrorModel, view_permission=ViewPermission()) 25 | return app, None, error_tracker 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_end_point.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test all end points are working as expected 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import unittest 10 | 11 | import pyquery 12 | from .utils import TestCaseMixin 13 | from .configs import pagination_config 14 | 15 | 16 | class ViewTest(TestCaseMixin): 17 | db_prefix = "ViewTestCase" 18 | config_module = pagination_config 19 | 20 | def test_list_view(self): 21 | db_name = "test_list_view" 22 | app, db, _ = self.setUpApp(db_name) 23 | with app.test_client() as c: 24 | c.get('/value-error') 25 | c.post('/post-view') 26 | response = c.get('/dev/error', follow_redirects=True) 27 | html = response.data 28 | urls = [node.attrib['href'] for node in pyquery.PyQuery(html)('a')] 29 | # 2 links for delete operation and 2 links to navigate 30 | self.assertEqual(len(urls), 2 + 2) 31 | 32 | urls = [node.attrib['href'] for node in pyquery.PyQuery(html)('.view-link')] 33 | self.assertEqual(len(urls), 2) 34 | 35 | def test_detail_view(self): 36 | db_name = "test_detail_view" 37 | app, db, _ = self.setUpApp(db_name) 38 | with app.test_client() as c: 39 | c.get('/value-error') 40 | response = c.get('/dev/error', follow_redirects=True) 41 | html = response.data 42 | url = [node.attrib['href'] for node in pyquery.PyQuery(html)('.view-link')][0] 43 | response = c.get(url) 44 | row = pyquery.PyQuery(response.data)('.row')[0] 45 | # row = etree.tostring(row, pretty_print=False) 46 | p = pyquery.PyQuery(row)('p') 47 | divs = pyquery.PyQuery(row)('.row div') 48 | self.assertEqual(len(p), 5) 49 | self.assertEqual(len(divs), 2) 50 | 51 | def test_delete_view(self): 52 | db_name = "test_delete_view" 53 | app, db, error_tracker = self.setUpApp(db_name) 54 | with app.test_client() as c: 55 | c.get('/value-error') 56 | response = c.get('/dev/error', follow_redirects=True) 57 | html = response.data 58 | url = [node.attrib['href'] for node in pyquery.PyQuery(html)('.delete')][0] 59 | c.get(url, follow_redirects=True) 60 | self.assertEqual(len(error_tracker.get_exceptions()), 0) 61 | 62 | def test_pagination(self): 63 | db_name = "test_pagination" 64 | app, db, error_tracker = self.setUpApp(db_name) 65 | distinct_exceptions = set() 66 | 67 | with app.test_client() as c: 68 | c.get('/value-error') 69 | exception = error_tracker.get_exceptions()[0] 70 | hashx = exception.hash 71 | inserted = 0 72 | while inserted < 20: 73 | inserted += 1 74 | idx = str(inserted) + hashx[2:] 75 | print("Inserting ", idx) 76 | error_tracker.create_or_update_exception(idx, exception.host, exception.path, 77 | exception.method, exception.request_data, 78 | exception.exception_name, 79 | exception.traceback) 80 | 81 | 82 | response = c.get('/dev/error', follow_redirects=True) 83 | data = response.data 84 | urls = [node.attrib['href'] for node in pyquery.PyQuery(data)('a')] 85 | self.assertEqual(len(urls), pagination_config.APP_DEFAULT_LIST_SIZE * 2 + 1) 86 | self.assertTrue('/dev/error/?page=2' in urls) 87 | 88 | response = c.get('/dev/error?page=2', follow_redirects=True) 89 | urls = [node.attrib['href'] for node in pyquery.PyQuery(response.data)('a')] 90 | self.assertEqual(len(urls), pagination_config.APP_DEFAULT_LIST_SIZE * 2 + 2) 91 | self.assertTrue('/dev/error/?page=1' in urls) 92 | self.assertTrue('/dev/error/?page=3' in urls) 93 | 94 | response = c.get('/dev/error?page=5', follow_redirects=True) 95 | urls = [node.attrib['href'] for node in pyquery.PyQuery(response.data)('a')] 96 | self.assertTrue('/dev/error/?page=4' in urls) 97 | 98 | response = c.get('/dev/error?page=6', follow_redirects=True) 99 | urls = [node.attrib['href'] for node in pyquery.PyQuery(response.data)('a')] 100 | self.assertEqual(len(urls), 1) 101 | 102 | 103 | if __name__ == '__main__': 104 | unittest.main() 105 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_init_later.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test app initialization post constructions 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import unittest 10 | 11 | from .test_basic import BasicTest 12 | from flask import Flask 13 | from flask_sqlalchemy import SQLAlchemy 14 | from tests.utils import ViewPermission 15 | from error_tracker import AppErrorTracker 16 | 17 | 18 | class InitLaterTest(BasicTest): 19 | db_prefix = "InitLaterTest" 20 | 21 | def _setup(self, db_file): 22 | app = Flask(__name__) 23 | app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///%s" % db_file 24 | db = SQLAlchemy(app) 25 | error_tracker = AppErrorTracker() 26 | error_tracker.init_app(app, db, view_permission=ViewPermission()) 27 | with app.app_context(): 28 | db.drop_all() 29 | db.create_all() 30 | return app, db, error_tracker 31 | 32 | 33 | if __name__ == '__main__': 34 | unittest.main() 35 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_manager_crud.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test all end points are working as expected 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | from .utils import TestCaseMixin 9 | 10 | 11 | class AppErrorTrackerCrudTest(TestCaseMixin): 12 | db_prefix = "AppErrorTrackerCrudTest" 13 | 14 | def fire_request(self, db_name): 15 | app, db, error_tracker = self.setUpApp(db_name) 16 | with app.test_client() as c: 17 | form_data = dict( 18 | username="username", 19 | password="password") 20 | c.post('/post-view', data=form_data) 21 | return error_tracker 22 | 23 | def test_delete(self): 24 | error_tracker = self.fire_request("test_delete") 25 | error_tracker.delete_exception(error_tracker.get_exceptions()[0].hash) 26 | self.assertEqual(len(error_tracker.get_exceptions()), 0) 27 | 28 | def test_get(self): 29 | error_tracker = self.fire_request("test_get") 30 | rhash = error_tracker.get_exception(error_tracker.get_exceptions()[0].hash) 31 | self.assertIsNotNone(rhash) 32 | 33 | def test_create_or_update(self): 34 | error_tracker = self.fire_request("test_get") 35 | exception = error_tracker.get_exceptions()[0] 36 | exception.hash = "test_create_or_update" 37 | error_tracker.create_or_update_exception(exception.hash, exception.host, exception.path, 38 | exception.method, exception.request_data, 39 | exception.exception_name, exception.traceback) 40 | self.assertIsNotNone(error_tracker.get_exception(exception.hash)) 41 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_manual_error_tracking.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test manual error tracking is working or not 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | import unittest 9 | 10 | from tests.FlaskTest.utils import TestCaseMixin 11 | from error_tracker import flask_scope 12 | 13 | 14 | class RecordErrorTest(TestCaseMixin, unittest.TestCase): 15 | db_prefix = "RecordErrorTest" 16 | 17 | def throw(self): 18 | password = "qwerty" 19 | secret = "pass" 20 | key = "key" 21 | foo_secret = "THIS IS SECRET" 22 | test_password_test = "test_password_test" 23 | TestPassWordTest = "TestPassWordTest" 24 | TestSecret = "TESTSECRET" 25 | l = [1, 2, 3, 4] 26 | t = (1, 2, 3, 4) 27 | d = {'test': 100, "1": 1000} 28 | print(password, secret, key, d, foo_secret, 29 | TestPassWordTest, test_password_test, TestSecret, l, t, d) 30 | print(d['KeyError']) 31 | return "KeyError" 32 | 33 | def test_auto_track_exception_decorator(self): 34 | db_name = "test_tickets_are_raise" 35 | app, db, error_tracker = self.setUpApp(db_name) 36 | 37 | @error_tracker.auto_track_exception 38 | def fun(a, b, x="123"): 39 | print(a, b, x) 40 | self.throw() 41 | 42 | try: 43 | fun(1, 2, x="test") 44 | except Exception as e: 45 | pass 46 | self.verify(error_tracker) 47 | 48 | def verify(self, error_tracker, request_date="{}"): 49 | errors = error_tracker.get_exceptions() 50 | self.assertEqual(len(errors), 1) 51 | error = errors[-1] 52 | self.assertIsNotNone(error.hash) 53 | self.assertEqual(error.count, 1) 54 | self.assertEqual(error.method, '') 55 | self.assertEqual(error.path, "") 56 | self.assertEqual(error.host, "") 57 | self.assertEqual(error.request_data, request_date) 58 | self.assertIsNotNone(error.traceback) 59 | self.assertIsNotNone(error.created_on) 60 | self.assertIsNotNone(error.last_seen) 61 | 62 | def test_record(self): 63 | db_name = "test_tickets_are_raise" 64 | app, db, error_tracker = self.setUpApp(db_name) 65 | try: 66 | self.throw() 67 | except Exception as e: 68 | error_tracker.capture_exception() 69 | self.verify(error_tracker) 70 | 71 | def test_message(self): 72 | db_name = "test_message" 73 | app, db, error_tracker = self.setUpApp(db_name) 74 | try: 75 | self.throw() 76 | except Exception as e: 77 | error_tracker.capture_message("Something went wrong!") 78 | self.verify(error_tracker, request_date="{'context': {'message': 'Something went wrong!'}}") 79 | 80 | def test_context_manager(self): 81 | db_name = "context_manager" 82 | app, db, error_tracker = self.setUpApp(db_name) 83 | with flask_scope(error_tracker) as scope: 84 | scope.set_extra("id", 1234) 85 | self.throw() 86 | self.verify(error_tracker, request_date="{'context': {'id': 1234}}") 87 | 88 | def test_context_manager_with_initial_context(self): 89 | db_name = "test_context_manager_with_initial_context" 90 | app, db, error_tracker = self.setUpApp(db_name) 91 | with flask_scope(error_tracker, context={"id": 1234}) as scope: 92 | self.throw() 93 | self.verify(error_tracker, request_date="{'context': {'id': 1234}}") 94 | 95 | def test_context_manager_exception_not_handled(self): 96 | db_name = "test_context_manager_exception_not_handled" 97 | app, db, error_tracker = self.setUpApp(db_name) 98 | try: 99 | with flask_scope(error_tracker, handle_exception=False) as scope: 100 | self.throw() 101 | self.assertTrue(False) 102 | except Exception: 103 | self.verify(error_tracker, request_date="{}") 104 | 105 | 106 | if __name__ == '__main__': 107 | unittest.main() 108 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_mask_rule.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Masking rule tests 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import re 10 | import unittest 11 | 12 | from error_tracker.flask.defaults import APP_ERROR_MASK_WITH 13 | from tests.utils import Masking 14 | from .utils import TestCaseMixin 15 | from .configs import masking_disabled, custom_mask_rule 16 | 17 | 18 | class DefaultMaskingRuleTest(TestCaseMixin): 19 | db_prefix = "DefaultMaskingRule" 20 | 21 | def test_mask_key(self): 22 | db_name = "test_mask_key" 23 | app, db, error_tracker = self.setUpApp(db_name) 24 | with app.test_client() as c: 25 | c.post('/post-view') 26 | errors = error_tracker.get_exceptions() 27 | error = errors[0] 28 | re1 = r".*password.* = .*" 29 | re2 = r".*secret.* = .*" 30 | re1 = re.compile(re1, re.IGNORECASE) 31 | re2 = re.compile(re2, re.IGNORECASE) 32 | exception = error.traceback 33 | matches1 = re1.findall(exception) 34 | matches2 = re2.findall(exception) 35 | self.assertEqual(len(matches1), 3) 36 | self.assertEqual(len(matches2), 3) 37 | for match in matches2 + matches1: 38 | key, value = match.split(" = ") 39 | self.assertEqual(value, "%r" % APP_ERROR_MASK_WITH) 40 | 41 | def test_mask_key_form(self): 42 | db_name = "test_mask_key_form" 43 | app, db, error_tracker = self.setUpApp(db_name) 44 | with app.test_client() as c: 45 | form_data = dict( 46 | username="username", 47 | password="password") 48 | c.post('/post-view', data=form_data) 49 | errors = error_tracker.get_exceptions() 50 | error = errors[0] 51 | re1 = r".*password.* = .*" 52 | re2 = r".*secret.* = .*" 53 | re1 = re.compile(re1, re.IGNORECASE) 54 | re2 = re.compile(re2, re.IGNORECASE) 55 | exception = error.traceback 56 | print("EXCEPTION", exception) 57 | matches1 = re1.findall(exception) 58 | matches2 = re2.findall(exception) 59 | self.assertEqual(len(matches1), 3) 60 | self.assertEqual(len(matches2), 3) 61 | for match in matches2 + matches1: 62 | key, value = match.split(" = ") 63 | self.assertEqual(value, "%r" % APP_ERROR_MASK_WITH) 64 | 65 | re3 = r".*password.* : .*" 66 | re3 = re.compile(re3, re.IGNORECASE) 67 | 68 | matches3 = re3.findall(exception) 69 | self.assertEqual(len(matches3), 1) 70 | for match in matches3: 71 | words = match.split("' : ") 72 | key, value = words[0], words[1].split(",")[0] 73 | self.assertEqual(value, "%r" % APP_ERROR_MASK_WITH) 74 | 75 | 76 | class NoMaskingTest(TestCaseMixin): 77 | db_prefix = "NoMasking" 78 | config_module = masking_disabled 79 | 80 | def test_no_mask(self): 81 | db_name = "test_no_mask" 82 | app, db, error_tracker = self.setUpApp(db_name) 83 | with app.test_client() as c: 84 | form_data = dict( 85 | username="username", 86 | password="password") 87 | c.post('/post-view', data=form_data) 88 | errors = error_tracker.get_exceptions() 89 | error = errors[0] 90 | re1 = r".*password.* = .*" 91 | re2 = r".*secret.* = .*" 92 | re1 = re.compile(re1, re.IGNORECASE) 93 | re2 = re.compile(re2, re.IGNORECASE) 94 | exception = error.traceback 95 | matches1 = re1.findall(exception) 96 | matches2 = re2.findall(exception) 97 | self.assertEqual(len(matches1), 3) 98 | self.assertEqual(len(matches2), 3) 99 | for match in matches2 + matches1: 100 | key, value = match.split(" = ") 101 | self.assertNotEqual(value, "%r" % APP_ERROR_MASK_WITH) 102 | 103 | re3 = r".*password.* : .*" 104 | re3 = re.compile(re3, re.IGNORECASE) 105 | matches3 = re3.findall(exception) 106 | self.assertEqual(len(matches3), 1) 107 | for match in matches3: 108 | words = match.split("' : ") 109 | key, value = words[0], words[1].split(",")[0] 110 | self.assertNotEqual(value, "%r" % APP_ERROR_MASK_WITH) 111 | 112 | 113 | class CustomMaskingTest(object): 114 | def verify(self): 115 | db_name = "test_mask" 116 | app, db, error_tracker = self.setUpApp(db_name) 117 | with app.test_client() as c: 118 | form_data = dict( 119 | username="username", 120 | password="password") 121 | c.post('/post-view', data=form_data) 122 | errors = error_tracker.get_exceptions() 123 | error = errors[0] 124 | print(error.traceback) 125 | re1 = r".*password.* = .*" 126 | re2 = r".*secret.* = .*" 127 | re3 = r".*password.* : .*" 128 | re4 = r".*key.* = .*" 129 | re1 = re.compile(re1, re.IGNORECASE) 130 | re2 = re.compile(re2, re.IGNORECASE) 131 | re3 = re.compile(re3, re.IGNORECASE) 132 | re4 = re.compile(re4, re.IGNORECASE) 133 | exception = error.traceback 134 | matches1 = re1.findall(exception) 135 | matches2 = re2.findall(exception) 136 | matches3 = re3.findall(exception) 137 | matches4 = re4.findall(exception) 138 | 139 | self.assertEqual(len(matches1), 3) 140 | self.assertEqual(len(matches2), 3) 141 | self.assertEqual(len(matches3), 1) 142 | self.assertEqual(len(matches4), 1) 143 | 144 | for match in matches2 + matches1 + matches4: 145 | key, value = match.split(" = ") 146 | self.assertEqual(str(value), "%r" % custom_mask_rule.APP_ERROR_MASK_WITH) 147 | 148 | for match in matches3: 149 | key, value = match.split(",")[0].split(" : ") 150 | self.assertEqual(value, "%r" % custom_mask_rule.APP_ERROR_MASK_WITH) 151 | 152 | 153 | class CustomMaskingRuleTest(TestCaseMixin, CustomMaskingTest): 154 | db_prefix = "CustomMaskingRule" 155 | config_module = custom_mask_rule 156 | 157 | def test_mask(self): 158 | self.verify() 159 | 160 | 161 | class CustomMaskingClassTest(TestCaseMixin, CustomMaskingTest): 162 | db_prefix = "CustomMaskingClass" 163 | config_module = masking_disabled 164 | kwargs = dict(masking=Masking(custom_mask_rule.APP_ERROR_MASK_WITH, 165 | custom_mask_rule.APP_ERROR_MASKED_KEY_HAS)) 166 | 167 | def test_mask(self): 168 | self.verify() 169 | 170 | 171 | if __name__ == '__main__': 172 | unittest.main() 173 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_notification.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Notification feature tests 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import unittest 10 | from tests.utils import TestNotification 11 | from .utils import TestCaseMixin 12 | from .configs import notification_config_disabled 13 | from .configs import notification_config_enabled 14 | 15 | 16 | class NotificationDisabledTest(TestCaseMixin): 17 | db_prefix = "NotificationDisabled" 18 | config_module = notification_config_disabled 19 | notifier = TestNotification() 20 | kwargs = dict(notifier=notifier) 21 | 22 | def test_notification_disabled_flag(self): 23 | app, db, error_tracker = self.setUpApp("notification_disabled_flag") 24 | self.assertEqual(False, error_tracker.send_notification) 25 | 26 | def test_notification_disabled(self): 27 | app, db, error_tracker = self.setUpApp("notification_disabled") 28 | with app.test_client() as c: 29 | result = c.get('/value-error') 30 | self.assertEqual(u'500', result.data.decode('utf-8')) 31 | self.assertEqual(len(self.notifier.get_notifications()), 0) 32 | 33 | 34 | class NotificationConfigurationTests(object): 35 | mailer = None 36 | 37 | def email_send(self): 38 | db_name = "test_notification_send" 39 | app, db, error_tracker = self.setUpApp(db_name) 40 | with app.test_client() as c: 41 | result = c.get('/value-error') 42 | self.assertEqual(u'500', result.data.decode('utf-8')) 43 | self.assertEqual(len(self.mailer.get_notifications()), 1) 44 | notification = self.mailer.get_notifications()[0] 45 | self.assertTrue( 46 | notification[0].startswith("[" + notification_config_enabled.APP_ERROR_SUBJECT_PREFIX + "]")) 47 | c.get('/value-error') 48 | self.assertEqual(len(self.mailer.get_notifications()), 2) 49 | 50 | c.post('/value-error') 51 | 52 | self.assertEqual(len(self.mailer.get_notifications()), 3) 53 | self.mailer.clear() 54 | 55 | def notification_flag_enabled(self): 56 | db_name = "test_notification_enabled" 57 | app, db, error_tracker = self.setUpApp(db_name) 58 | self.assertEqual(True, error_tracker.send_notification) 59 | 60 | 61 | class NotificationEnabledTest(TestCaseMixin, NotificationConfigurationTests): 62 | db_prefix = "NotificationEnabled" 63 | mailer = TestNotification() 64 | kwargs = dict(notifier=mailer) 65 | config_module = notification_config_enabled 66 | 67 | def test_notification_flag_enabled(self): 68 | self.notification_flag_enabled() 69 | 70 | def test_email_send(self): 71 | self.email_send() 72 | 73 | 74 | class NotificationEnabledByAppInstanceTest(TestCaseMixin, NotificationConfigurationTests): 75 | db_prefix = "NotificationEnabledByAppInstance" 76 | mailer = TestNotification() 77 | kwargs = dict(notifier=mailer) 78 | config_module = notification_config_enabled 79 | 80 | def test_notification_flag_enabled(self): 81 | self.notification_flag_enabled() 82 | 83 | def test_email_send(self): 84 | self.email_send() 85 | 86 | 87 | if __name__ == '__main__': 88 | unittest.main() 89 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_ticketing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Ticketing feature test, this tests whether raise_ticket method is called or not. 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | 10 | import unittest 11 | from tests.utils import TicketingSystem 12 | from .utils import TestCaseMixin 13 | 14 | 15 | class TicketingTest(TestCaseMixin, unittest.TestCase): 16 | db_prefix = "Ticketing" 17 | ticketing = TicketingSystem() 18 | kwargs = dict(ticketing=ticketing) 19 | 20 | def test_tickets_are_raise(self): 21 | db_name = "test_tickets_are_raise" 22 | app, db, error_tracker = self.setUpApp(db_name) 23 | with app.test_client() as c: 24 | result = c.get('/value-error') 25 | self.assertEqual(u'500', result.data.decode('utf-8')) 26 | self.assertEqual(len(self.ticketing.get_tickets()), 1) 27 | c.get('/value-error') 28 | self.assertEqual(len(self.ticketing.get_tickets()), 2) 29 | 30 | c.post('/value-error') 31 | 32 | self.assertEqual(len(self.ticketing.get_tickets()), 3) 33 | self.ticketing.clear() 34 | 35 | 36 | if __name__ == '__main__': 37 | unittest.main() 38 | -------------------------------------------------------------------------------- /tests/FlaskTest/test_url_prefix.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # URL prefix test, this tests whether all urls are exposed at given path prefix or not 4 | # 5 | # :copyright: 2019 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import unittest 10 | from .configs import custom_mask_rule 11 | from .utils import TestCaseMixin 12 | 13 | 14 | class UrlPrefixTest(TestCaseMixin): 15 | db_prefix = "UrlPrefixTest" 16 | config_module = custom_mask_rule 17 | kwargs = dict(url_prefix="/dev/exception/") 18 | 19 | def test_url(self): 20 | db_name = "test_url" 21 | app, db, error_tracker = self.setUpApp(db_name) 22 | with app.test_client() as c: 23 | c.post('/post-view') 24 | response = c.get(self.kwargs['url_prefix']) 25 | self.assertEqual(response.status_code, 200) 26 | self.assertNotEqual(len(response.data), 0) 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /tests/FlaskTest/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test's util class 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | import logging 10 | from flask import Flask 11 | from flask import request 12 | from flask_sqlalchemy import SQLAlchemy 13 | from unittest import TestCase 14 | 15 | from error_tracker import AppErrorTracker 16 | from tests.utils import ViewPermission 17 | 18 | 19 | class BaseTestMixin(TestCase): 20 | log_file = "flask_error_tracker.log" 21 | db_prefix = "" 22 | 23 | def get_db_name(self, db_name): 24 | if len(self.db_prefix) != 0: 25 | return "%s-%s.sqlite" % (self.db_prefix, db_name) 26 | raise ValueError 27 | 28 | def _setup(self, db_name): 29 | raise NotImplemented 30 | 31 | def setUpApp(self, db_name): 32 | db_name = self.get_db_name(db_name) 33 | app, db, error_manager = self._setup(db_name) 34 | l = logging.getLogger(__name__) 35 | formatter = logging.Formatter('%(message)s') 36 | fileHandler = logging.FileHandler(self.log_file) 37 | fileHandler.setFormatter(formatter) 38 | streamHandler = logging.StreamHandler() 39 | streamHandler.setFormatter(formatter) 40 | l.setLevel(logging.DEBUG) 41 | l.addHandler(fileHandler) 42 | l.addHandler(streamHandler) 43 | 44 | self.logger = l 45 | 46 | @app.route('/') 47 | def index(): 48 | return u'No Exception!' 49 | 50 | @app.route("/value-error", methods=['GET', 'POST']) 51 | def view_value_error(): 52 | raise ValueError 53 | 54 | @app.route("/post-view", methods=['POST']) 55 | def post_view(): 56 | form = request.form 57 | password = "qwerty" 58 | secret = "pass" 59 | key = "key" 60 | foo_secret = "THIS IS SECRET" 61 | test_password_test = "test_password_test" 62 | TestPassWordTest = "TestPassWordTest" 63 | TestSecret = "TESTSECRET" 64 | l = [1, 2, 3, 4] 65 | t = (1, 2, 3, 4) 66 | d = {'test': 100, "1": 1000} 67 | print(form, password, secret, key, d, foo_secret, 68 | TestPassWordTest, test_password_test, TestSecret, l, t, d) 69 | print(d['KeyError']) 70 | return "KeyError" 71 | 72 | @app.errorhandler(500) 73 | @error_manager.track_exception 74 | def error_500(e): 75 | return u"500", 500 76 | 77 | return app, db, error_manager 78 | 79 | def write(self, data): 80 | with open("log.log", "a") as f: 81 | f.write("*" * 100) 82 | f.write("\n") 83 | f.write(str(data)) 84 | f.write("\n") 85 | f.write("*" * 100) 86 | f.write("\n") 87 | 88 | 89 | class TestCaseMixin(BaseTestMixin): 90 | config_module = None 91 | kwargs = dict() 92 | 93 | def _setup(self, db_file): 94 | app = Flask(__name__) 95 | if self.config_module is not None: 96 | app.config.from_object(self.config_module) 97 | app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///%s" % db_file 98 | db = SQLAlchemy(app) 99 | error_tracker = AppErrorTracker(app=app, db=db, view_permission=ViewPermission(), **self.kwargs) 100 | with app.app_context(): 101 | db.drop_all() 102 | db.create_all() 103 | return app, db, error_tracker 104 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonus21/error-tracker/13324bcf67abc14ea38c3d3d01fcd030ffe9ddc7/tests/__init__.py -------------------------------------------------------------------------------- /tests/flask-test-runner.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | sys.path.insert(0, BASE_DIR) 7 | 8 | from FlaskTest.test_basic import BasicTest 9 | from FlaskTest.test_db_model import CustomModelClassTest 10 | from FlaskTest.test_end_point import ViewTest 11 | from FlaskTest.test_init_later import InitLaterTest 12 | from FlaskTest.test_manager_crud import AppErrorTrackerCrudTest 13 | from FlaskTest.test_mask_rule import DefaultMaskingRuleTest, NoMaskingTest, CustomMaskingClassTest, \ 14 | CustomMaskingRuleTest 15 | from FlaskTest.test_notification import NotificationDisabledTest, NotificationEnabledByAppInstanceTest, \ 16 | NotificationEnabledTest 17 | from FlaskTest.test_ticketing import TicketingTest 18 | from FlaskTest.test_url_prefix import UrlPrefixTest 19 | from FlaskTest.test_manual_error_tracking import RecordErrorTest 20 | from FlaskTest.test_401_views import View401Test 21 | 22 | loader = unittest.TestLoader() 23 | suite = loader.loadTestsFromTestCase(BasicTest) 24 | suite.addTests(loader.loadTestsFromTestCase(CustomModelClassTest)) 25 | suite.addTests(loader.loadTestsFromTestCase(ViewTest)) 26 | suite.addTests(loader.loadTestsFromTestCase(InitLaterTest)) 27 | suite.addTests(loader.loadTestsFromTestCase(AppErrorTrackerCrudTest)) 28 | suite.addTests(loader.loadTestsFromTestCase(RecordErrorTest)) 29 | suite.addTests(loader.loadTestsFromTestCase(CustomMaskingClassTest)) 30 | suite.addTests(loader.loadTestsFromTestCase(CustomMaskingRuleTest)) 31 | suite.addTests(loader.loadTestsFromTestCase(NoMaskingTest)) 32 | suite.addTests(loader.loadTestsFromTestCase(DefaultMaskingRuleTest)) 33 | suite.addTests(loader.loadTestsFromTestCase(NotificationEnabledTest)) 34 | suite.addTests(loader.loadTestsFromTestCase(NotificationEnabledByAppInstanceTest)) 35 | suite.addTests(loader.loadTestsFromTestCase(NotificationDisabledTest)) 36 | suite.addTests(loader.loadTestsFromTestCase(TicketingTest)) 37 | suite.addTests(loader.loadTestsFromTestCase(UrlPrefixTest)) 38 | suite.addTests(loader.loadTestsFromTestCase(View401Test)) 39 | 40 | # initialize a runner, pass it your suite and run it 41 | runner = unittest.TextTestRunner(verbosity=2) 42 | result = runner.run(suite) 43 | sys.exit(bool(result.failures)) 44 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Test utility 4 | # 5 | # :copyright: 2023 Sonu Kumar 6 | # :license: BSD-3-Clause 7 | # 8 | 9 | 10 | import datetime 11 | from collections import namedtuple 12 | 13 | import six 14 | 15 | from error_tracker import ModelMixin, TicketingMixin, NotificationMixin, MaskingMixin, ViewPermissionMixin 16 | 17 | Error = namedtuple("Error", "hash, host, path, method, request_data, exception_name," 18 | " traceback, count, created_on, last_seen") 19 | paginator = namedtuple("Paginator", "has_next, has_prev, next_num, prev_num, items") 20 | 21 | 22 | class TestErrorModel(ModelMixin): 23 | objects = {} 24 | 25 | @classmethod 26 | def delete_entity(cls, rhash): 27 | cls.objects.pop(rhash) 28 | 29 | @classmethod 30 | def create_or_update_entity(cls, rhash, host, path, method, request_data, 31 | exception_name, traceback): 32 | count = 1 33 | now = datetime.datetime.now() 34 | created_on = now 35 | traceback = traceback 36 | 37 | if rhash in cls.objects: 38 | error = cls.objects[rhash] 39 | created_on = error.created_on 40 | exception_name = error.exception_name 41 | traceback = error.traceback 42 | count = error.count + 1 43 | error = Error(rhash, host, path, method, str(request_data), 44 | exception_name, traceback, count, created_on, now) 45 | cls.objects[rhash] = error 46 | return error 47 | 48 | @classmethod 49 | def get_exceptions_per_page(cls, page_number=1): 50 | return paginator(False, False, None, None, list(cls.objects.values())) 51 | 52 | @classmethod 53 | def get_entity(cls, rhash): 54 | error = cls.objects.get(rhash, None) 55 | return error 56 | 57 | @classmethod 58 | def delete_all(cls): 59 | cls.objects = {} 60 | 61 | 62 | class TestNotification(NotificationMixin): 63 | def __init__(self, *args, **kwargs): 64 | super(TestNotification, self).__init__(*args, **kwargs) 65 | self.emails = [] 66 | 67 | def notify(self, request, exception, 68 | email_subject=None, 69 | email_body=None, 70 | from_email=None, 71 | recipient_list=None): 72 | self.emails.append((email_subject, email_body)) 73 | 74 | def clear(self): 75 | self.emails = [] 76 | 77 | def get_notifications(self): 78 | return self.emails 79 | 80 | 81 | class TicketingSystem(TicketingMixin): 82 | tickets = [] 83 | 84 | def raise_ticket(self, object, request=None): 85 | self.tickets.append(object) 86 | 87 | def get_tickets(self): 88 | return self.tickets 89 | 90 | def clear(self): 91 | self.tickets = [] 92 | 93 | 94 | class Masking(MaskingMixin): 95 | def __call__(self, key): 96 | if isinstance(key, six.string_types): 97 | tmp_key = key.lower() 98 | for k in self.mask_key_has: 99 | if k in tmp_key: 100 | return True, "'%s'" % self.mask_with 101 | return False, None 102 | 103 | 104 | class ViewPermission(ViewPermissionMixin): 105 | def __call__(self, request): 106 | return True 107 | --------------------------------------------------------------------------------