├── .coveragerc ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── .pre-commit-config.yaml ├── AUTHORS ├── CHANGELOG ├── INSTALL ├── LICENCE ├── MANIFEST.in ├── README.rst ├── VERSION ├── demo ├── MANIFEST.in ├── README ├── demoproject │ ├── __init__.py │ ├── manage.py │ ├── settings.py │ ├── templates │ │ └── error_test.html │ ├── urls.py │ └── wsgi.py └── setup.py ├── django_js_error_hook ├── __init__.py ├── models.py ├── static │ └── js │ │ └── django_js_error_hook.js ├── tests.py ├── urls.py └── views.py ├── manage.py ├── setup.py ├── test_settings.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = *django_js_error_hook* 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true 5 | }, 6 | extends: [ 7 | 'standard' 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 'latest' 11 | }, 12 | rules: { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 2 | 3 | name: CI 4 | 5 | on: [ push, pull_request ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python: [ "3.6", "3.7", "3.8", "3.9", "3.10" ] 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | python -m pip install tox tox-gh-actions 23 | - name: Test with tox 24 | run: tox 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | pre-commit: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-python@v3 17 | - uses: pre-commit/action@v3.0.0 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | *.egg-info 4 | dist/ 5 | env/ 6 | demo/__pycache__/ 7 | *.sqlite3 8 | .direnv/ 9 | node_modules/ 10 | .envrc 11 | package.json 12 | package-lock.json 13 | .tox/ 14 | build/ 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.10 3 | repos: 4 | - repo: https://github.com/psf/black 5 | rev: 22.3.0 6 | hooks: 7 | - id: black 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v4.1.0 10 | hooks: 11 | - id: check-ast 12 | - id: check-merge-conflict 13 | - id: check-case-conflict 14 | - id: detect-private-key 15 | - id: check-added-large-files 16 | - id: check-json 17 | - id: check-symlinks 18 | - id: check-toml 19 | - id: end-of-file-fixer 20 | - id: trailing-whitespace 21 | - id: mixed-line-ending 22 | args: [--fix=lf] 23 | - repo: https://github.com/asottile/reorder_python_imports 24 | rev: v3.0.1 25 | hooks: 26 | - id: reorder-python-imports 27 | args: 28 | - --py3-plus 29 | - --application-directories=.:src 30 | exclude: migrations/ 31 | - repo: https://github.com/pre-commit/pygrep-hooks 32 | rev: v1.9.0 33 | hooks: 34 | - id: python-check-blanket-noqa 35 | - id: python-check-mock-methods 36 | - id: python-no-eval 37 | - id: python-no-log-warn 38 | - id: rst-backticks 39 | - repo: https://github.com/asottile/pyupgrade 40 | rev: v2.31.1 41 | hooks: 42 | - id: pyupgrade 43 | args: 44 | - --py310-plus 45 | exclude: migrations/ 46 | - repo: https://github.com/adamchainz/django-upgrade 47 | rev: 1.4.0 48 | hooks: 49 | - id: django-upgrade 50 | args: 51 | - --target-version=4.0 52 | - repo: https://github.com/asottile/yesqa 53 | rev: v1.3.0 54 | hooks: 55 | - id: yesqa 56 | - repo: https://github.com/hadialqattan/pycln 57 | rev: v1.2.5 58 | hooks: 59 | - id: pycln 60 | - repo: https://github.com/pycqa/flake8 61 | rev: 4.0.1 62 | hooks: 63 | - id: flake8 64 | exclude: | 65 | (?x)^( 66 | .*/migrations/.* 67 | )$ 68 | additional_dependencies: 69 | - flake8-bugbear 70 | - flake8-comprehensions 71 | - flake8-tidy-imports 72 | - flake8-print 73 | args: [--max-line-length=120] 74 | - repo: https://github.com/pre-commit/mirrors-eslint 75 | rev: v8.25.0 76 | hooks: 77 | - id: eslint 78 | args: [--fix] 79 | additional_dependencies: 80 | - "eslint@8.25.0" 81 | - "eslint-plugin-prettier@4.2.1" 82 | - "eslint-config-standard@17.0.0" 83 | - "eslint-plugin-import@2.26.0" 84 | - "eslint-plugin-n@15.3.0" 85 | - "eslint-plugin-promise@6.1.0" 86 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | ###################### 2 | Authors & contributors 3 | ###################### 4 | 5 | * Bang Yongbae (Brice) 6 | * Ionel Cristian Mărieș 7 | * Jeroen van Oorschot 8 | * Johannes Wilm 9 | * Jonathan Dorival 10 | * Julien Bouquillon 11 | * Keryn Knight 12 | * Petr Dlouhý 13 | * Phoebe Bright 14 | * Richard Barran 15 | * Rémy Hubscher 16 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.0.1 (unreleased) 5 | ------------------ 6 | 7 | - Nothing changed yet. 8 | 9 | 10 | 1.0 (2022-10-17) 11 | ---------------- 12 | 13 | - Switched CI tests from Travis to Github Actions. 14 | - Add support for Django 4.0. 15 | - Use static to serve the utility. 16 | - Add pre-commit and linter. 17 | 18 | 19 | 0.8 (2019-12-08) 20 | ---------------- 21 | 22 | - Respect logging level (#17) 23 | 24 | 25 | 0.7 (2019-03-26) 26 | ---------------- 27 | 28 | - Handle unhandledrejections with non error reasons (#13) 29 | - Allow blacklisting of custom useragents and errors (#14) 30 | - Handle missing user in the request (#15) 31 | 32 | 33 | 0.6 (2018-01-10) 34 | ---------------- 35 | 36 | - Port to Django 2.0 37 | - Add unhandled promise rejection (#12) 38 | 39 | 40 | 0.5 (2017-09-04) 41 | ---------------- 42 | 43 | - Add column number and error_object and make requests asynchronous. (#10) 44 | 45 | 0.4 (2016-03-18) 46 | ---------------- 47 | 48 | - Made usable with Django 1.9 and Python 3 (#7) 49 | 50 | 51 | 0.3 (2014-01-11) 52 | ---------------- 53 | 54 | - Request info is provided to the logger. 55 | 56 | 57 | 0.2 (2013-11-26) 58 | ---------------- 59 | 60 | - Remove jQuery dependency 61 | - Add csrf validation 62 | 63 | 64 | 0.1 (2012-09-06) 65 | ---------------- 66 | 67 | - Project initialization. 68 | - Add utils.js generated from template 69 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | See README 2 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | ####### 2 | License 3 | ####### 4 | 5 | Copyright (c) 2012, Benoît Bryon. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of wardrobe nor the names of its contributors 19 | may be used to endorse or promote products derived from this software without 20 | specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include django_js_error_hook * 2 | global-exclude *.pyc .*.swp 3 | include AUTHORS CHANGELOG INSTALL LICENSE README.rst VERSION 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ####### 2 | INSTALL 3 | ####### 4 | 5 | To run the demo project for testing:: 6 | 7 | $ git clone git://github.com/jojax/django-js-error-hook.git 8 | $ cd django-js-error-hook 9 | $ virtualenv env --python=python3 10 | $ source env/bin/activate 11 | (env) $ pip install -e . 12 | (env) $ pip install -e demo 13 | (env) $ demo migrate 14 | 15 | Run the server:: 16 | 17 | (env) $ demo runserver 18 | 19 | Then access: http://localhost:8000/ - the JavaScript error will be logged in your console. 20 | 21 | To install the project in production:: 22 | 23 | $ pip install django-js-error-hook 24 | 25 | Add django-js-error-hook to your INSTALLED_APPS settings:: 26 | 27 | INSTALLED_APPS = ( 28 | ... 29 | 'django.contrib.staticfiles', 30 | 'django_js_error_hook', 31 | ... 32 | ) 33 | 34 | If you want to log the error in the console for development:: 35 | 36 | LOGGING = { 37 | 'version': 1, 38 | 'disable_existing_loggers': False, 39 | 'filters': { 40 | 'require_debug_false': { 41 | '()': 'django.utils.log.RequireDebugFalse' 42 | } 43 | }, 44 | 'formatters': { 45 | 'verbose': { 46 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 47 | }, 48 | 'simple': { 49 | 'format': '\033[22;32m%(levelname)s\033[0;0m %(message)s' 50 | }, 51 | }, 52 | 'handlers': { 53 | 'mail_admins': { 54 | 'level': 'ERROR', 55 | 'filters': ['require_debug_false'], 56 | 'class': 'django.utils.log.AdminEmailHandler' 57 | }, 58 | 'console': { 59 | 'level': 'DEBUG', 60 | 'class': 'logging.StreamHandler', 61 | 'formatter': 'simple' 62 | }, 63 | }, 64 | 'loggers': { 65 | 'django.request': { 66 | 'handlers': ['mail_admins'], 67 | 'level': 'ERROR', 68 | 'propagate': True, 69 | }, 70 | 'javascript_error': { 71 | 'handlers': ['mail_admins', 'console'], 72 | 'level': 'ERROR', 73 | 'propagate': True, 74 | }, 75 | } 76 | } 77 | 78 | By default the logger is called "javascript_error", if you want you can define ``JAVASCRIPT_ERROR_ID`` in your settings:: 79 | 80 | JAVASCRIPT_ERROR_ID = '' 81 | 82 | The view will do csrf validation - if for some reason it doesn't work, set ``JAVASCRIPT_ERROR_CSRF_EXEMPT`` to ``True`` in your settings. 83 | 84 | Then install the urls:: 85 | 86 | urlpatterns = patterns('', 87 | ... 88 | url(r'^js_error_hook/', include('django_js_error_hook.urls')), 89 | ... 90 | ) 91 | 92 | 93 | In your template, simply add the js_error_hook script:: 94 | 95 | 98 | 99 | 100 | Now every JavaScript error will be logged in your logging error stream. (Mail, Sentry, ...) 101 | 102 | Have fun and feel free to fork us and give us feedbacks! 103 | 104 | ########### 105 | DEVELOPMENT 106 | ########### 107 | When writing for this app you can run `tox `_ which will test the project 108 | against various versions of Python and Django: 109 | 110 | pip install tox 111 | tox 112 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.1.dev0 2 | -------------------------------------------------------------------------------- /demo/MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include demoproject/templates * 2 | global-exclude *.pyc .*.swp 3 | -------------------------------------------------------------------------------- /demo/README: -------------------------------------------------------------------------------- 1 | Demo project for Django-JS-error-hook 2 | -------------------------------------------------------------------------------- /demo/demoproject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jojax/django-js-error-hook/542135b187f37b390f3fe3208e06eacd23c6d0f4/demo/demoproject/__init__.py -------------------------------------------------------------------------------- /demo/demoproject/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | 6 | def main(): 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demoproject.settings") 8 | try: 9 | from django.core.management import execute_from_command_line 10 | except ImportError as exc: 11 | raise ImportError( 12 | "Couldn't import Django. Are you sure it's installed and " 13 | "available on your PYTHONPATH environment variable? Did you " 14 | "forget to activate a virtual environment?" 15 | ) from exc 16 | execute_from_command_line(sys.argv) 17 | 18 | 19 | if __name__ == "__main__": 20 | main() 21 | -------------------------------------------------------------------------------- /demo/demoproject/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for demo project. 2 | from os.path import dirname 3 | from os.path import join 4 | 5 | demoproject_dir = dirname(__file__) 6 | 7 | DEBUG = True 8 | 9 | ADMINS = ( 10 | # ('Your Name', 'your_email@example.com'), 11 | ) 12 | 13 | MANAGERS = ADMINS 14 | 15 | DATABASES = { 16 | "default": { 17 | "ENGINE": "django.db.backends.sqlite3", 18 | "NAME": "test.sqlite3", 19 | }, 20 | } 21 | 22 | # Local time zone for this installation. Choices can be found here: 23 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 24 | # although not all choices may be available on all operating systems. 25 | # In a Windows environment this must be set to your system time zone. 26 | TIME_ZONE = "America/Chicago" 27 | 28 | # Language code for this installation. All choices can be found here: 29 | # http://www.i18nguy.com/unicode/language-identifiers.html 30 | LANGUAGE_CODE = "en-us" 31 | 32 | SITE_ID = 1 33 | 34 | # If you set this to False, Django will make some optimizations so as not 35 | # to load the internationalization machinery. 36 | USE_I18N = True 37 | 38 | # If you set this to False, Django will not format dates, numbers and 39 | # calendars according to the current locale. 40 | USE_L10N = True 41 | 42 | # If you set this to False, Django will not use timezone-aware datetimes. 43 | USE_TZ = True 44 | 45 | # Absolute filesystem path to the directory that will hold user-uploaded files. 46 | # Example: "/home/media/media.lawrence.com/media/" 47 | MEDIA_ROOT = "" 48 | 49 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 50 | # trailing slash. 51 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 52 | MEDIA_URL = "" 53 | 54 | # Absolute path to the directory static files should be collected to. 55 | # Don't put anything in this directory yourself; store your static files 56 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 57 | # Example: "/home/media/media.lawrence.com/static/" 58 | STATIC_ROOT = "" 59 | 60 | # URL prefix for static files. 61 | # Example: "http://media.lawrence.com/static/" 62 | STATIC_URL = "/static/" 63 | 64 | # Additional locations of static files 65 | STATICFILES_DIRS = ( 66 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 67 | # Always use forward slashes, even on Windows. 68 | # Don't forget to use absolute paths, not relative paths. 69 | ) 70 | 71 | # List of finder classes that know how to find static files in 72 | # various locations. 73 | STATICFILES_FINDERS = ( 74 | "django.contrib.staticfiles.finders.FileSystemFinder", 75 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 76 | ) 77 | 78 | # Make this unique, and don't share it with anybody. 79 | SECRET_KEY = "k9i055*z@6@9$7xvyw(8y4sk_w0@1ltf2$y-^zu^&wnlt1oez5" 80 | 81 | TEMPLATES = [ 82 | { 83 | "BACKEND": "django.template.backends.django.DjangoTemplates", 84 | "APP_DIRS": True, 85 | "DIRS": (join(demoproject_dir, "templates"),), 86 | }, 87 | ] 88 | 89 | MIDDLEWARE = ( 90 | "django.middleware.common.CommonMiddleware", 91 | "django.contrib.sessions.middleware.SessionMiddleware", 92 | "django.middleware.csrf.CsrfViewMiddleware", 93 | "django.contrib.auth.middleware.AuthenticationMiddleware", 94 | "django.contrib.messages.middleware.MessageMiddleware", 95 | # Uncomment the next line for simple clickjacking protection: 96 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 97 | ) 98 | 99 | ROOT_URLCONF = "demoproject.urls" 100 | 101 | # Python dotted path to the WSGI application used by Django's runserver. 102 | WSGI_APPLICATION = "demoproject.wsgi.application" 103 | 104 | INSTALLED_APPS = ( 105 | "django.contrib.auth", 106 | "django.contrib.contenttypes", 107 | "django.contrib.sessions", 108 | "django.contrib.sites", 109 | "django.contrib.messages", 110 | "django.contrib.staticfiles", 111 | "django_js_error_hook", 112 | # Uncomment the next line to enable the admin: 113 | # 'django.contrib.admin', 114 | # Uncomment the next line to enable admin documentation: 115 | # 'django.contrib.admindocs', 116 | ) 117 | 118 | JAVASCRIPT_ERROR_CSRF_EXEMPT = True 119 | 120 | # A sample logging configuration. The only tangible logging 121 | # performed by this configuration is to send an email to 122 | # the site admins on every HTTP 500 error when DEBUG=False. 123 | # See http://docs.djangoproject.com/en/dev/topics/logging for 124 | # more details on how to customize your logging configuration. 125 | LOGGING = { 126 | "version": 1, 127 | "disable_existing_loggers": False, 128 | "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, 129 | "formatters": { 130 | "verbose": { 131 | "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s", 132 | }, 133 | "simple": {"format": "\033[22;32m%(levelname)s\033[0;0m %(message)s"}, 134 | }, 135 | "handlers": { 136 | "mail_admins": { 137 | "level": "ERROR", 138 | "filters": ["require_debug_false"], 139 | "class": "django.utils.log.AdminEmailHandler", 140 | }, 141 | "console": { 142 | "level": "DEBUG", 143 | "class": "logging.StreamHandler", 144 | "formatter": "simple", 145 | }, 146 | }, 147 | "loggers": { 148 | "django.request": { 149 | "handlers": ["mail_admins"], 150 | "level": "ERROR", 151 | "propagate": True, 152 | }, 153 | "javascript_error": { 154 | "handlers": ["mail_admins", "console"], 155 | "level": "ERROR", 156 | "propagate": True, 157 | }, 158 | }, 159 | } 160 | -------------------------------------------------------------------------------- /demo/demoproject/templates/error_test.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | Error JS Test view 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | A simple Page which voluntary triggers a regular JavaScript error and an unhandled promise rejection error (Ctrl+U to see the script causing the errors). 18 | 21 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/demoproject/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include 2 | from django.urls import path 3 | from django.views.generic import TemplateView 4 | 5 | urlpatterns = [ 6 | path("", TemplateView.as_view(template_name="error_test.html")), 7 | path("error_hook/", include("django_js_error_hook.urls")), 8 | ] 9 | -------------------------------------------------------------------------------- /demo/demoproject/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demoproject. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | from django.core.wsgi import get_wsgi_application 19 | 20 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 21 | 22 | # This application object is used by any WSGI server configured to use this 23 | # file. This includes Django's development server, if the WSGI_APPLICATION 24 | # setting points here. 25 | application = get_wsgi_application() 26 | 27 | # Apply WSGI middleware here. 28 | # from helloworld.wsgi import HelloWorldApplication 29 | # application = HelloWorldApplication(application) 30 | -------------------------------------------------------------------------------- /demo/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | 6 | def read_relative_file(filename): 7 | """Returns contents of the given file, which path is supposed relative 8 | to this module.""" 9 | with open(os.path.join(os.path.dirname(__file__), filename)) as f: 10 | return f.read() 11 | 12 | 13 | NAME = "django-js-error-hook-demo" 14 | README = read_relative_file("README") 15 | VERSION = 0.1 16 | PACKAGES = ["demoproject"] 17 | REQUIRES = ["django-js-error-hook"] 18 | 19 | 20 | setup( 21 | name=NAME, 22 | version=VERSION, 23 | description="Demo project for django-js-error-hook.", 24 | long_description=README, 25 | classifiers=[ 26 | "Development Status :: 1 - Planning", 27 | "License :: OSI Approved :: BSD License", 28 | "Programming Language :: Python :: 3", 29 | "Framework :: Django", 30 | ], 31 | keywords="class-based view, generic view, js error hooking", 32 | author="Jonathan Dorival", 33 | author_email="jonathan.dorival@novapost.fr", 34 | url="https://github.com/jojax/%s" % NAME, 35 | license="BSD", 36 | packages=PACKAGES, 37 | include_package_data=True, 38 | zip_safe=False, 39 | install_requires=REQUIRES, 40 | entry_points={ 41 | "console_scripts": [ 42 | "demo = demoproject.manage:main", 43 | ], 44 | }, 45 | ) 46 | -------------------------------------------------------------------------------- /django_js_error_hook/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jojax/django-js-error-hook/542135b187f37b390f3fe3208e06eacd23c6d0f4/django_js_error_hook/__init__.py -------------------------------------------------------------------------------- /django_js_error_hook/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jojax/django-js-error-hook/542135b187f37b390f3fe3208e06eacd23c6d0f4/django_js_error_hook/models.py -------------------------------------------------------------------------------- /django_js_error_hook/static/js/django_js_error_hook.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function getCookie (name) { 3 | if (!document.cookie || document.cookie === '') { 4 | return null 5 | } 6 | const cookie = document.cookie.split(';').map(cookie => cookie.trim()).find(cookie => { 7 | if (cookie.substring(0, name.length + 1) === (name + '=')) { 8 | return true 9 | } else { 10 | return false 11 | } 12 | }) 13 | if (cookie) { 14 | return decodeURIComponent(cookie.substring(name.length + 1)) 15 | } 16 | return null 17 | } 18 | function logError (details) { 19 | const cookie = getCookie('csrftoken') 20 | const data = { 21 | context: String(navigator.userAgent), 22 | details: String(details) 23 | } 24 | 25 | if (window.fetch) { 26 | const body = new FormData() 27 | body.append('context', data.context) 28 | body.append('details', data.details) 29 | 30 | return fetch(window.djangoJSErrorHandlerUrl, { 31 | method: 'POST', 32 | headers: { 33 | 'X-CSRFToken': cookie 34 | }, 35 | credentials: 'include', 36 | body 37 | }) 38 | } else { 39 | const xhr = new XMLHttpRequest() 40 | 41 | xhr.open('POST', window.djangoJSErrorHandlerUrl, true) 42 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded') 43 | 44 | if (cookie) { 45 | xhr.setRequestHeader('X-CSRFToken', cookie) 46 | } 47 | const query = [] 48 | for (const key in data) { 49 | query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key])) 50 | } 51 | xhr.send(query.join('&')) 52 | } 53 | } 54 | 55 | window.onerror = function (msg, url, lineNumber, columnNumber, errorObj) { 56 | let logMessage = url + ': ' + lineNumber + ': ' + msg 57 | if (columnNumber) { 58 | logMessage += ', ' + columnNumber 59 | } 60 | if (errorObj && errorObj.stack) { 61 | logMessage += ', ' + errorObj.stack 62 | } 63 | logError(logMessage) 64 | } 65 | 66 | if (window.addEventListener) { 67 | window.addEventListener('unhandledrejection', function (rejection) { 68 | let logMessage = rejection.type 69 | if (rejection.reason) { 70 | if (rejection.reason.message) { 71 | logMessage += ', ' + rejection.reason.message 72 | } else { 73 | logMessage += ', ' + JSON.stringify(rejection.reason) 74 | } 75 | if (rejection.reason.stack) { 76 | logMessage += ', ' + rejection.reason.stack 77 | } 78 | } 79 | logError(logMessage) 80 | }) 81 | } 82 | })() 83 | -------------------------------------------------------------------------------- /django_js_error_hook/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | 4 | 5 | class JSErrorHookTestCase(TestCase): 6 | """Test project views.""" 7 | 8 | def test_error_handler_view(self): 9 | """A POST should log the error""" 10 | response = self.client.post( 11 | reverse("js-error-handler"), 12 | { 13 | "context": ( 14 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " 15 | "(KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36" 16 | ), 17 | "details": "Description of the error by the browser javascript engine.", 18 | }, 19 | ) 20 | self.assertEqual(response.status_code, 200) 21 | -------------------------------------------------------------------------------- /django_js_error_hook/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from .views import js_error_view 4 | 5 | 6 | urlpatterns = [ 7 | re_path("^$", js_error_view, name="js-error-handler"), 8 | ] 9 | -------------------------------------------------------------------------------- /django_js_error_hook/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.http import HttpResponse 5 | from django.views.decorators.csrf import csrf_exempt 6 | from django.views.generic import View 7 | 8 | ERROR_ID = getattr(settings, "JAVASCRIPT_ERROR_ID", "javascript_error") 9 | CSRF_EXEMPT = getattr(settings, "JAVASCRIPT_ERROR_CSRF_EXEMPT", False) 10 | BLACKLIST_USERAGENT = getattr( 11 | settings, 12 | "JAVASCRIPT_ERROR_USERAGENT_BLACKLIST", 13 | ["googlebot", "bingbot"], 14 | ) 15 | BLACKLIST_ERRORS = getattr(settings, "JAVASCRIPT_ERROR_BLACKLIST", []) 16 | 17 | logger = logging.getLogger(ERROR_ID) 18 | 19 | 20 | class JSErrorHandlerView(View): 21 | """View that take the JS error as POST parameters and log it""" 22 | 23 | def post(self, request): 24 | """Read POST data and log it as an JS error""" 25 | error_dict = request.POST.dict() 26 | if hasattr(request, "user"): 27 | error_dict["user"] = ( 28 | request.user if request.user.is_authenticated else "" 29 | ) 30 | else: 31 | error_dict["user"] = "" 32 | 33 | level = logging.ERROR 34 | if any( 35 | useragent in error_dict["context"].lower() 36 | for useragent in BLACKLIST_USERAGENT 37 | ) or any(error in error_dict["details"].lower() for error in BLACKLIST_ERRORS): 38 | level = logging.WARNING 39 | 40 | logger.log( 41 | level, 42 | "Got error: \n%s", 43 | "\n".join(f"\t{key}: {value}" for key, value in error_dict.items()), 44 | extra={"status_code": 500, "request": request}, 45 | ) 46 | return HttpResponse("Error logged") 47 | 48 | 49 | if CSRF_EXEMPT: 50 | js_error_view = csrf_exempt(JSErrorHandlerView.as_view()) 51 | else: 52 | js_error_view = JSErrorHandlerView.as_view() 53 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "%s.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Python packaging.""" 2 | import os 3 | 4 | from setuptools import setup 5 | 6 | 7 | def read_relative_file(filename): 8 | """Returns contents of the given file, which path is supposed relative 9 | to this module.""" 10 | with open(os.path.join(os.path.dirname(__file__), filename)) as f: 11 | return f.read() 12 | 13 | 14 | NAME = "django-js-error-hook" 15 | README = read_relative_file("README.rst") 16 | VERSION = read_relative_file("VERSION").strip() 17 | PACKAGES = ["django_js_error_hook"] 18 | REQUIRES = ["django>=3.2.0"] 19 | 20 | 21 | setup( 22 | name=NAME, 23 | version=VERSION, 24 | description="Generic handler for hooking client side javascript error.", 25 | long_description=README, 26 | classifiers=[ 27 | "Development Status :: 1 - Planning", 28 | "License :: OSI Approved :: BSD License", 29 | "Programming Language :: Python :: 3", 30 | "Framework :: Django", 31 | ], 32 | keywords="class-based view, generic view, js error hooking", 33 | author="Jonathan Dorival", 34 | author_email="jonathan.dorival@novapost.fr", 35 | url="https://github.com/jojax/%s" % NAME, 36 | license="BSD", 37 | packages=PACKAGES, 38 | include_package_data=True, 39 | zip_safe=False, 40 | install_requires=REQUIRES, 41 | ) 42 | -------------------------------------------------------------------------------- /test_settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for demoproject project. 2 | from os.path import dirname 3 | 4 | demoproject_dir = dirname(__file__) 5 | 6 | DEBUG = True 7 | 8 | ADMINS = ( 9 | # ('Your Name', 'your_email@example.com'), 10 | ) 11 | 12 | MANAGERS = ADMINS 13 | 14 | DATABASES = { 15 | "default": { 16 | "ENGINE": "django.db.backends.sqlite3", # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 17 | "NAME": "test.sqlite3", # Or path to database file if using sqlite3. 18 | "USER": "", # Not used with sqlite3. 19 | "PASSWORD": "", # Not used with sqlite3. 20 | "HOST": "", # Set to empty string for localhost. Not used with sqlite3. 21 | "PORT": "", # Set to empty string for default. Not used with sqlite3. 22 | }, 23 | } 24 | 25 | # Local time zone for this installation. Choices can be found here: 26 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 27 | # although not all choices may be available on all operating systems. 28 | # In a Windows environment this must be set to your system time zone. 29 | TIME_ZONE = "America/Chicago" 30 | 31 | # Language code for this installation. All choices can be found here: 32 | # http://www.i18nguy.com/unicode/language-identifiers.html 33 | LANGUAGE_CODE = "en-us" 34 | 35 | SITE_ID = 1 36 | 37 | # If you set this to False, Django will make some optimizations so as not 38 | # to load the internationalization machinery. 39 | USE_I18N = True 40 | 41 | # If you set this to False, Django will not format dates, numbers and 42 | # calendars according to the current locale. 43 | USE_L10N = True 44 | 45 | # If you set this to False, Django will not use timezone-aware datetimes. 46 | USE_TZ = True 47 | 48 | # Absolute filesystem path to the directory that will hold user-uploaded files. 49 | # Example: "/home/media/media.lawrence.com/media/" 50 | MEDIA_ROOT = "" 51 | 52 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 53 | # trailing slash. 54 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 55 | MEDIA_URL = "" 56 | 57 | # Absolute path to the directory static files should be collected to. 58 | # Don't put anything in this directory yourself; store your static files 59 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 60 | # Example: "/home/media/media.lawrence.com/static/" 61 | STATIC_ROOT = "" 62 | 63 | # URL prefix for static files. 64 | # Example: "http://media.lawrence.com/static/" 65 | STATIC_URL = "/static/" 66 | 67 | # Additional locations of static files 68 | STATICFILES_DIRS = ( 69 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 70 | # Always use forward slashes, even on Windows. 71 | # Don't forget to use absolute paths, not relative paths. 72 | ) 73 | 74 | # List of finder classes that know how to find static files in 75 | # various locations. 76 | STATICFILES_FINDERS = ( 77 | "django.contrib.staticfiles.finders.FileSystemFinder", 78 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 79 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 80 | ) 81 | 82 | # Make this unique, and don't share it with anybody. 83 | SECRET_KEY = "k9i055*z@6@9$7xvyw(8y4sk_w0@1ltf2$y-^zu^&wnlt1oez5" 84 | 85 | TEMPLATES = [ 86 | { 87 | "BACKEND": "django.template.backends.django.DjangoTemplates", 88 | "APP_DIRS": True, 89 | }, 90 | ] 91 | 92 | MIDDLEWARE = ( 93 | "django.middleware.common.CommonMiddleware", 94 | "django.contrib.sessions.middleware.SessionMiddleware", 95 | "django.middleware.csrf.CsrfViewMiddleware", 96 | "django.contrib.auth.middleware.AuthenticationMiddleware", 97 | "django.contrib.messages.middleware.MessageMiddleware", 98 | # Uncomment the next line for simple clickjacking protection: 99 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 100 | ) 101 | 102 | ROOT_URLCONF = "django_js_error_hook.urls" 103 | 104 | # Python dotted path to the WSGI application used by Django's runserver. 105 | WSGI_APPLICATION = "demoproject.wsgi.application" 106 | 107 | INSTALLED_APPS = ( 108 | "django.contrib.auth", 109 | "django.contrib.contenttypes", 110 | "django.contrib.sessions", 111 | "django.contrib.sites", 112 | "django.contrib.messages", 113 | "django.contrib.staticfiles", 114 | "django_js_error_hook", 115 | # Uncomment the next line to enable the admin: 116 | # 'django.contrib.admin', 117 | # Uncomment the next line to enable admin documentation: 118 | # 'django.contrib.admindocs', 119 | ) 120 | 121 | # A sample logging configuration. The only tangible logging 122 | # performed by this configuration is to send an email to 123 | # the site admins on every HTTP 500 error when DEBUG=False. 124 | # See http://docs.djangoproject.com/en/dev/topics/logging for 125 | # more details on how to customize your logging configuration. 126 | LOGGING = { 127 | "version": 1, 128 | "disable_existing_loggers": False, 129 | "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, 130 | "formatters": { 131 | "verbose": { 132 | "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s", 133 | }, 134 | "simple": {"format": "\033[22;32m%(levelname)s\033[0;0m %(message)s"}, 135 | }, 136 | "handlers": { 137 | "mail_admins": { 138 | "level": "ERROR", 139 | "filters": ["require_debug_false"], 140 | "class": "django.utils.log.AdminEmailHandler", 141 | }, 142 | "console": { 143 | "level": "DEBUG", 144 | "class": "logging.StreamHandler", 145 | "formatter": "simple", 146 | }, 147 | }, 148 | "loggers": { 149 | "django.request": { 150 | "handlers": ["mail_admins"], 151 | "level": "ERROR", 152 | "propagate": True, 153 | }, 154 | "javascript_error": { 155 | "handlers": ["mail_admins", "console"], 156 | "level": "ERROR", 157 | "propagate": True, 158 | }, 159 | }, 160 | } 161 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | {py36,py37,py38,py39,py310}-django32, 4 | {py38,py39,py310}-django40, 5 | {py38,py39,py310}-django41, 6 | 7 | [testenv] 8 | deps= 9 | django32: Django>=3.2.0,<4.0.0 10 | django40: Django>=4.0.0,<4.1.0 11 | django41: Django>=4.1.0,<4.2.0 12 | 13 | commands= python manage.py test --settings=test_settings 14 | 15 | [gh-actions] 16 | python = 17 | 3.6: py36 18 | 3.7: py37 19 | 3.8: py38 20 | 3.9: py39 21 | 3.10: py310 22 | --------------------------------------------------------------------------------