diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..acc93a0
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,14 @@
+module.exports = {
+ env: {
+ browser: true,
+ es2021: true
+ },
+ extends: [
+ 'standard'
+ ],
+ parserOptions: {
+ ecmaVersion: 'latest'
+ },
+ rules: {
+ }
+}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b62a147..8466fcb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,24 +9,16 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python: [ 3.5, 3.6, 3.7 ]
+ python: [ "3.6", "3.7", "3.8", "3.9", "3.10" ]
steps:
- - uses: actions/checkout@v2
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
- with:
- python-version: ${{ matrix.python }}
- - name: Install dependencies
- run: pip install tox tox-gh-actions
- - name: Run tests
- run: tox
- - name: Coverage
- if: ${{ matrix.python == 3.7 }}
- run: |
- pip install coverage[toml] django==2.0
- coverage run manage.py test --settings=test_settings
- - name: Upload coverage
- if: ${{ matrix.python == 3.7 }}
- uses: codecov/codecov-action@v1
- with:
- name: Python ${{ matrix.python }}
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install tox tox-gh-actions
+ - name: Test with tox
+ run: tox
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..dfc86c3
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,17 @@
+name: lint
+
+on:
+ push:
+ branches:
+ - "*"
+ pull_request:
+ branches:
+ - "*"
+
+jobs:
+ pre-commit:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v3
+ - uses: pre-commit/action@v3.0.0
diff --git a/.gitignore b/.gitignore
index 60ae115..546d446 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,11 @@
dist/
env/
demo/__pycache__/
+demo/build/
*.sqlite3
+.direnv/
+node_modules/
+.envrc
+package.json
+package-lock.json
+.tox/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..d2b46ee
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,91 @@
+default_language_version:
+ python: python3.10
+repos:
+ - repo: https://github.com/psf/black
+ rev: 22.3.0
+ hooks:
+ - id: black
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.1.0
+ hooks:
+ - id: check-ast
+ - id: check-merge-conflict
+ - id: check-case-conflict
+ - id: detect-private-key
+ - id: check-added-large-files
+ - id: check-json
+ - id: check-symlinks
+ - id: check-toml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - id: mixed-line-ending
+ args: [--fix=lf]
+ - repo: https://github.com/asottile/reorder_python_imports
+ rev: v3.0.1
+ hooks:
+ - id: reorder-python-imports
+ args:
+ - --py3-plus
+ - --application-directories=.:src
+ exclude: migrations/
+ - repo: https://github.com/pre-commit/pygrep-hooks
+ rev: v1.9.0
+ hooks:
+ - id: python-check-blanket-noqa
+ - id: python-check-mock-methods
+ - id: python-no-eval
+ - id: python-no-log-warn
+ - id: rst-backticks
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v2.31.1
+ hooks:
+ - id: pyupgrade
+ args:
+ - --py310-plus
+ exclude: migrations/
+ - repo: https://github.com/adamchainz/django-upgrade
+ rev: 1.4.0
+ hooks:
+ - id: django-upgrade
+ args:
+ - --target-version=4.0
+ - repo: https://github.com/asottile/yesqa
+ rev: v1.3.0
+ hooks:
+ - id: yesqa
+ - repo: https://github.com/asottile/add-trailing-comma
+ rev: v2.2.2
+ hooks:
+ - id: add-trailing-comma
+ args:
+ - --py36-plus
+ - repo: https://github.com/hadialqattan/pycln
+ rev: v1.2.5
+ hooks:
+ - id: pycln
+ - repo: https://github.com/pycqa/flake8
+ rev: 4.0.1
+ hooks:
+ - id: flake8
+ exclude: |
+ (?x)^(
+ .*/migrations/.*
+ )$
+ additional_dependencies:
+ - flake8-bugbear
+ - flake8-comprehensions
+ - flake8-tidy-imports
+ - flake8-print
+ args: [--max-line-length=120]
+ - repo: https://github.com/pre-commit/mirrors-eslint
+ rev: v8.25.0
+ hooks:
+ - id: eslint
+ args: [--fix]
+ additional_dependencies:
+ - "eslint@8.25.0"
+ - "eslint-plugin-prettier@4.2.1"
+ - "eslint-config-standard@17.0.0"
+ - "eslint-plugin-import@2.26.0"
+ - "eslint-plugin-n@15.3.0"
+ - "eslint-plugin-promise@6.1.0"
diff --git a/LICENCE b/LICENCE
index 8d53d77..6c3c8df 100644
--- a/LICENCE
+++ b/LICENCE
@@ -28,4 +28,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.rst b/README.rst
index 44b9cea..a5bce60 100644
--- a/README.rst
+++ b/README.rst
@@ -6,7 +6,7 @@ To run the demo project for testing::
$ git clone git://github.com/jojax/django-js-error-hook.git
$ cd django-js-error-hook
- $ virtualenv env --python=python3
+ $ python -m venv env
$ source env/bin/activate
(env) $ pip install -e .
(env) $ pip install -e demo
@@ -92,7 +92,10 @@ Then install the urls::
In your template, simply add the js_error_hook script::
-
+
+
Now every JavaScript error will be logged in your logging error stream. (Mail, Sentry, ...)
@@ -106,4 +109,3 @@ against various versions of Python and Django:
pip install tox
tox
-
diff --git a/demo/demoproject/manage.py b/demo/demoproject/manage.py
index ae1ab0e..fb4635b 100755
--- a/demo/demoproject/manage.py
+++ b/demo/demoproject/manage.py
@@ -11,7 +11,7 @@ def main():
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
- "forget to activate a virtual environment?"
+ "forget to activate a virtual environment?",
) from exc
execute_from_command_line(sys.argv)
diff --git a/demo/demoproject/settings.py b/demo/demoproject/settings.py
index 911862e..94711f7 100644
--- a/demo/demoproject/settings.py
+++ b/demo/demoproject/settings.py
@@ -1,6 +1,8 @@
# Django settings for demo project.
-from os.path import abspath, dirname, join
-demoproject_dir = dirname(abspath(__file__))
+from os.path import dirname
+from os.path import join
+
+demoproject_dir = dirname(__file__)
DEBUG = True
@@ -11,21 +13,21 @@
MANAGERS = ADMINS
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': 'test.sqlite3',
- }
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": "test.sqlite3",
+ },
}
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
-TIME_ZONE = 'America/Chicago'
+TIME_ZONE = "America/Chicago"
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
SITE_ID = 1
@@ -42,22 +44,22 @@
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
-MEDIA_ROOT = ''
+MEDIA_ROOT = ""
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
-MEDIA_URL = ''
+MEDIA_URL = ""
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
-STATIC_ROOT = ''
+STATIC_ROOT = ""
# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
# Additional locations of static files
STATICFILES_DIRS = (
@@ -69,46 +71,44 @@
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
- 'django.contrib.staticfiles.finders.FileSystemFinder',
- 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+ "django.contrib.staticfiles.finders.FileSystemFinder",
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
)
# Make this unique, and don't share it with anybody.
-SECRET_KEY = 'k9i055*z@6@9$7xvyw(8y4sk_w0@1ltf2$y-^zu^&wnlt1oez5'
+SECRET_KEY = "k9i055*z@6@9$7xvyw(8y4sk_w0@1ltf2$y-^zu^&wnlt1oez5"
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'APP_DIRS': True,
- 'DIRS': (
- join(demoproject_dir, 'templates'),
- )
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "APP_DIRS": True,
+ "DIRS": (join(demoproject_dir, "templates"),),
},
]
MIDDLEWARE = (
- 'django.middleware.common.CommonMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
+ "django.middleware.common.CommonMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
-ROOT_URLCONF = 'demoproject.urls'
+ROOT_URLCONF = "demoproject.urls"
# Python dotted path to the WSGI application used by Django's runserver.
-WSGI_APPLICATION = 'demoproject.wsgi.application'
+WSGI_APPLICATION = "demoproject.wsgi.application"
INSTALLED_APPS = (
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.sites',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'django_js_error_hook',
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.sites",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "django_js_error_hook",
# Uncomment the next line to enable the admin:
# 'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
@@ -123,43 +123,37 @@
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'filters': {
- 'require_debug_false': {
- '()': 'django.utils.log.RequireDebugFalse'
- }
- },
- 'formatters': {
- 'verbose': {
- 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
- },
- 'simple': {
- 'format': '\033[22;32m%(levelname)s\033[0;0m %(message)s'
+ "version": 1,
+ "disable_existing_loggers": False,
+ "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
+ "formatters": {
+ "verbose": {
+ "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s",
},
+ "simple": {"format": "\033[22;32m%(levelname)s\033[0;0m %(message)s"},
},
- 'handlers': {
- 'mail_admins': {
- 'level': 'ERROR',
- 'filters': ['require_debug_false'],
- 'class': 'django.utils.log.AdminEmailHandler'
+ "handlers": {
+ "mail_admins": {
+ "level": "ERROR",
+ "filters": ["require_debug_false"],
+ "class": "django.utils.log.AdminEmailHandler",
+ },
+ "console": {
+ "level": "DEBUG",
+ "class": "logging.StreamHandler",
+ "formatter": "simple",
},
- 'console': {
- 'level': 'DEBUG',
- 'class': 'logging.StreamHandler',
- 'formatter': 'simple'
- },
},
- 'loggers': {
- 'django.request': {
- 'handlers': ['mail_admins'],
- 'level': 'ERROR',
- 'propagate': True,
+ "loggers": {
+ "django.request": {
+ "handlers": ["mail_admins"],
+ "level": "ERROR",
+ "propagate": True,
},
- 'javascript_error': {
- 'handlers': ['mail_admins', 'console'],
- 'level': 'ERROR',
- 'propagate': True,
+ "javascript_error": {
+ "handlers": ["mail_admins", "console"],
+ "level": "ERROR",
+ "propagate": True,
},
- }
+ },
}
diff --git a/demo/demoproject/tests/__init__.py b/demo/demoproject/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/demo/demoproject/tests/helpers.py b/demo/demoproject/tests/helpers.py
new file mode 100644
index 0000000..5384976
--- /dev/null
+++ b/demo/demoproject/tests/helpers.py
@@ -0,0 +1,75 @@
+import logging
+import os
+
+from django.contrib.staticfiles.testing import StaticLiveServerTestCase
+from django.core.servers.basehttp import WSGIRequestHandler
+from django.core.servers.basehttp import WSGIServer
+from django.test.testcases import LiveServerThread
+from selenium import webdriver
+from selenium.webdriver.chrome.service import Service as ChromiumService
+from webdriver_manager.chrome import ChromeDriverManager
+from webdriver_manager.core.utils import ChromeType
+
+
+logger = logging.getLogger(__name__)
+
+
+class VerboseLiveServerTestCase(StaticLiveServerTestCase):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ @classmethod
+ def setUpClass(cls):
+ options = webdriver.ChromeOptions()
+ if os.getenv("CI"):
+ # Github Actions
+ options.binary_location = "/usr/bin/google-chrome-stable"
+ options.add_argument("--headless")
+ options.add_argument("--window-size=1920,1080")
+ options.add_argument("--disable-gpu")
+ cls.wait_time = 20
+ else:
+ # Ubuntu Desktop, etc.
+ cls.wait_time = 6
+ cls.driver = webdriver.Chrome(
+ service=ChromiumService(
+ ChromeDriverManager(chrome_type=ChromeType.GOOGLE).install(),
+ ),
+ options=options,
+ )
+ cls.driver.implicitly_wait(cls.wait_time)
+ super().setUpClass()
+
+ def tearDown(self):
+ # Source: https://stackoverflow.com/a/39606065
+ if hasattr(self._outcome, "errors"):
+ # Python 3.4 - 3.10 (These two methods have no side effects)
+ result = self.defaultTestResult()
+ self._feedErrorsToResult(result, self._outcome.errors)
+ else:
+ # Python 3.11+
+ result = self._outcome.result
+ ok = all(test != self for test, text in result.errors + result.failures)
+ if not ok:
+ if not os.path.exists("screenshots"):
+ os.makedirs("screenshots")
+ screenshotfile = f"screenshots/{self._testMethodName}.png"
+ logger.info(f"Saving {screenshotfile}")
+ self.driver.save_screenshot(screenshotfile)
+ return super().tearDown()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.driver.quit()
+ super().tearDownClass()
+
+ class VersboseLiveServerThread(LiveServerThread):
+ def _create_server(self):
+ WSGIRequestHandler.handle = WSGIRequestHandler.handle_one_request
+ return WSGIServer(
+ (self.host, self.port),
+ WSGIRequestHandler,
+ allow_reuse_address=False,
+ )
+
+ server_thread_class = VersboseLiveServerThread
diff --git a/demo/demoproject/tests/test_backend.py b/demo/demoproject/tests/test_backend.py
new file mode 100644
index 0000000..f5ae9bc
--- /dev/null
+++ b/demo/demoproject/tests/test_backend.py
@@ -0,0 +1,20 @@
+from django.test import TestCase
+from django.urls import reverse
+
+
+class BackendTestCase(TestCase):
+ """Test project views."""
+
+ def test_error_handler_view(self):
+ """A POST should log the error"""
+ response = self.client.post(
+ reverse("js-error-handler"),
+ {
+ "context": (
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"
+ ),
+ "details": "Description of the error by the browser javascript engine.",
+ },
+ )
+ self.assertEqual(response.status_code, 200)
diff --git a/demo/demoproject/tests/test_frontend.py b/demo/demoproject/tests/test_frontend.py
new file mode 100644
index 0000000..f844bbc
--- /dev/null
+++ b/demo/demoproject/tests/test_frontend.py
@@ -0,0 +1,10 @@
+import time
+
+from .helpers import VerboseLiveServerTestCase
+
+
+class InteractionTest(VerboseLiveServerTestCase):
+ def test_error_in_browser(self):
+ self.driver.maximize_window()
+ self.driver.get(self.live_server_url + "/")
+ time.sleep(1000)
diff --git a/demo/demoproject/urls.py b/demo/demoproject/urls.py
index d4e445e..84282a3 100644
--- a/demo/demoproject/urls.py
+++ b/demo/demoproject/urls.py
@@ -1,7 +1,8 @@
-from django.conf.urls import include, url
+from django.urls import include
+from django.urls import path
from django.views.generic import TemplateView
urlpatterns = [
- url('^$', TemplateView.as_view(template_name="error_test.html")),
- url('^error_hook/', include('django_js_error_hook.urls')),
+ path("", TemplateView.as_view(template_name="error_test.html")),
+ path("error_hook/", include("django_js_error_hook.urls")),
]
diff --git a/demo/demoproject/wsgi.py b/demo/demoproject/wsgi.py
index 9c101ed..3cc6f83 100644
--- a/demo/demoproject/wsgi.py
+++ b/demo/demoproject/wsgi.py
@@ -14,6 +14,7 @@
"""
import os
+
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
diff --git a/demo/setup.py b/demo/setup.py
index 41ad75f..ec2db97 100644
--- a/demo/setup.py
+++ b/demo/setup.py
@@ -1,4 +1,5 @@
import os
+
from setuptools import setup
@@ -9,33 +10,36 @@ def read_relative_file(filename):
return f.read()
-NAME = 'django-js-error-hook-demo'
-README = read_relative_file('README')
+NAME = "django-js-error-hook-demo"
+README = read_relative_file("README")
VERSION = 0.1
-PACKAGES = ['demoproject']
-REQUIRES = ['django-js-error-hook']
+PACKAGES = ["demoproject", "demoproject.tests"]
+REQUIRES = ["django-js-error-hook"]
-setup(name=NAME,
- version=VERSION,
- description='Demo project for django-js-error-hook.',
- long_description=README,
- classifiers=['Development Status :: 1 - Planning',
- 'License :: OSI Approved :: BSD License',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 2.6',
- 'Framework :: Django'],
- keywords='class-based view, generic view, js error hooking',
- author='Jonathan Dorival',
- author_email='jonathan.dorival@novapost.fr',
- url='https://github.com/jojax/%s' % NAME,
- license='BSD',
- packages=PACKAGES,
- include_package_data=True,
- zip_safe=False,
- install_requires=REQUIRES,
- entry_points={
- 'console_scripts': [
- 'demo = demoproject.manage:main',
- ]
- })
+setup(
+ name=NAME,
+ version=VERSION,
+ description="Demo project for django-js-error-hook.",
+ long_description=README,
+ classifiers=[
+ "Development Status :: 1 - Planning",
+ "License :: OSI Approved :: BSD License",
+ "Programming Language :: Python :: 3",
+ "Framework :: Django",
+ ],
+ keywords="class-based view, generic view, js error hooking",
+ author="Jonathan Dorival",
+ author_email="jonathan.dorival@novapost.fr",
+ url="https://github.com/jojax/%s" % NAME,
+ license="BSD",
+ packages=PACKAGES,
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=REQUIRES,
+ entry_points={
+ "console_scripts": [
+ "demo = demoproject.manage:main",
+ ],
+ },
+)
diff --git a/demo/test_requirements.txt b/demo/test_requirements.txt
new file mode 100644
index 0000000..d3280eb
--- /dev/null
+++ b/demo/test_requirements.txt
@@ -0,0 +1,2 @@
+selenium
+webdriver-manager
diff --git a/django_js_error_hook/static/django_js_error_hook/utils.js b/django_js_error_hook/static/django_js_error_hook/utils.js
new file mode 100644
index 0000000..1c55f9d
--- /dev/null
+++ b/django_js_error_hook/static/django_js_error_hook/utils.js
@@ -0,0 +1,60 @@
+(function () {
+ function getCookie (name) {
+ const nameEQ = name + '='
+ const ca = document.cookie.split(';')
+ for (let i = 0; i < ca.length; i++) {
+ let c = ca[i]
+ while (c.charAt(0) === ' ') c = c.substring(1, c.length)
+ if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length)
+ }
+ return null
+ }
+
+ function logError (details) {
+ const xhr = new XMLHttpRequest()
+
+ xhr.open('POST', window.djangoJSErrorHandlerUrl, true)
+ xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
+ const cookie = getCookie('csrftoken')
+ if (cookie) {
+ xhr.setRequestHeader('X-CSRFToken', cookie)
+ }
+ const query = []
+ const data = {
+ context: navigator.userAgent,
+ details
+ }
+ for (const key in data) {
+ query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
+ }
+ xhr.send(query.join('&'))
+ }
+
+ window.onerror = function (msg, url, lineNumber, columnNumber, errorObj) {
+ let logMessage = url + ': ' + lineNumber + ': ' + msg
+ if (columnNumber) {
+ logMessage += ', ' + columnNumber
+ }
+ if (errorObj && errorObj.stack) {
+ logMessage += ', ' + errorObj.stack
+ }
+ logError(logMessage)
+ }
+
+ if (window.addEventListener) {
+ window.addEventListener('unhandledrejection', function (rejection) {
+ let logMessage = rejection.type
+ if (rejection.reason) {
+ if (rejection.reason.message) {
+ logMessage += ', ' + rejection.reason.message
+ } else {
+ logMessage += ', ' + JSON.stringify(rejection.reason)
+ }
+ if (rejection.reason.stack) {
+ logMessage += ', ' + rejection.reason.stack
+ }
+ }
+ logError(logMessage)
+ })
+ }
+})()
diff --git a/django_js_error_hook/templates/django_js_error_hook/utils.js b/django_js_error_hook/templates/django_js_error_hook/utils.js
deleted file mode 100644
index eca72ed..0000000
--- a/django_js_error_hook/templates/django_js_error_hook/utils.js
+++ /dev/null
@@ -1,58 +0,0 @@
-(function() {
- function getCookie(name) {
- var nameEQ = name + "=";
- var ca = document.cookie.split(';');
- for(var i=0;i < ca.length;i++) {
- var c = ca[i];
- while (c.charAt(0)==' ') c = c.substring(1,c.length);
- if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
- }
- return null;
- }
- function logError(details) {
- var xhr = new XMLHttpRequest();
-
- xhr.open("POST", "{% url 'js-error-handler' %}", true);
- xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
- var cookie = getCookie('csrftoken');
- if (cookie) {
- xhr.setRequestHeader("X-CSRFToken", cookie);
- }
- var query = [], data = {
- context: navigator.userAgent,
- details: details
- };
- for (var key in data) {
- query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
- }
- xhr.send(query.join('&'));
- }
-
- window.onerror = function(msg, url, line_number, column_number, error_obj) {
- var log_message = url + ': ' + line_number + ': ' + msg;
- if (column_number) {
- log_message += ", " + column_number;
- }
- if (error_obj && error_obj.stack) {
- log_message += ", " + error_obj.stack;
- }
- logError(log_message);
- };
-
- if (window.addEventListener) {
- window.addEventListener('unhandledrejection', function(rejection) {
- var log_message = rejection.type;
- if (rejection.reason) {
- if (rejection.reason.message) {
- log_message += ", " + rejection.reason.message;
- } else {
- log_message += ", " + JSON.stringify(rejection.reason);
- }
- if (rejection.reason.stack) {
- log_message += ", " + rejection.reason.stack;
- }
- }
- logError(log_message);
- })
- }
-})();
diff --git a/django_js_error_hook/tests.py b/django_js_error_hook/tests.py
deleted file mode 100644
index cda5e89..0000000
--- a/django_js_error_hook/tests.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import unittest
-from django.test import TestCase
-from django.urls import reverse
-
-class JSErrorHookTestCase(TestCase):
- """Test project views."""
-
- @unittest.skip('Noticed test fails as part of the github-actions PR - this needs fixing in a separate ticket.')
- def test_error_handler_view(self):
- """A POST should log the error"""
- response = self.client.post(reverse('js-error-handler'), {"details": "Description of the error by the browser javascript engine."})
- self.assertEqual(response.status_code, 200)
-
- def test_error_js_utils_view(self):
- response = self.client.get(reverse('js-error-handler-js'))
- self.assertEqual(response.status_code, 200)
diff --git a/django_js_error_hook/urls.py b/django_js_error_hook/urls.py
index a336fe9..ca8ca40 100644
--- a/django_js_error_hook/urls.py
+++ b/django_js_error_hook/urls.py
@@ -1,8 +1,8 @@
from django.urls import re_path
-from .views import js_error_view, utils_js
+
+from .views import js_error_view
urlpatterns = [
re_path("^$", js_error_view, name="js-error-handler"),
- re_path("^utils.js$", utils_js, name="js-error-handler-js"),
]
diff --git a/django_js_error_hook/views.py b/django_js_error_hook/views.py
index c41619c..2cb7d8e 100644
--- a/django_js_error_hook/views.py
+++ b/django_js_error_hook/views.py
@@ -1,17 +1,18 @@
-from distutils.version import StrictVersion
+import logging
-from django import get_version
from django.conf import settings
from django.http import HttpResponse
-from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt
-from django.views.generic import TemplateView, View
-import logging
+from django.views.generic import View
-ERROR_ID = getattr(settings, 'JAVASCRIPT_ERROR_ID', 'javascript_error')
-CSRF_EXEMPT = getattr(settings, 'JAVASCRIPT_ERROR_CSRF_EXEMPT', False)
-BLACKLIST_USERAGENT = getattr(settings, 'JAVASCRIPT_ERROR_USERAGENT_BLACKLIST', ['googlebot', 'bingbot'])
-BLACKLIST_ERRORS = getattr(settings, 'JAVASCRIPT_ERROR_BLACKLIST', [])
+ERROR_ID = getattr(settings, "JAVASCRIPT_ERROR_ID", "javascript_error")
+CSRF_EXEMPT = getattr(settings, "JAVASCRIPT_ERROR_CSRF_EXEMPT", False)
+BLACKLIST_USERAGENT = getattr(
+ settings,
+ "JAVASCRIPT_ERROR_USERAGENT_BLACKLIST",
+ ["googlebot", "bingbot"],
+)
+BLACKLIST_ERRORS = getattr(settings, "JAVASCRIPT_ERROR_BLACKLIST", [])
logger = logging.getLogger(ERROR_ID)
@@ -22,48 +23,28 @@ class JSErrorHandlerView(View):
def post(self, request):
"""Read POST data and log it as an JS error"""
error_dict = request.POST.dict()
- if hasattr(request, 'user'):
- error_dict['user'] = request.user if request.user.is_authenticated else ""
+ if hasattr(request, "user"):
+ error_dict["user"] = (
+ request.user if request.user.is_authenticated else ""
+ )
else:
- error_dict['user'] = ""
-
+ error_dict["user"] = ""
+
level = logging.ERROR
- if any(useragent in error_dict['context'].lower() for useragent in BLACKLIST_USERAGENT) or \
- any(error in error_dict['details'].lower() for error in BLACKLIST_ERRORS):
+ if any(
+ useragent in error_dict["context"].lower()
+ for useragent in BLACKLIST_USERAGENT
+ ) or any(error in error_dict["details"].lower() for error in BLACKLIST_ERRORS):
level = logging.WARNING
logger.log(
level,
- "Got error: \n%s", '\n'.join("\t%s: %s" % (key, value) for key, value in error_dict.items()),
- extra={
- 'status_code': 500,
- 'request': request
- }
+ "Got error: \n%s",
+ "\n".join(f"\t{key}: {value}" for key, value in error_dict.items()),
+ extra={"status_code": 500, "request": request},
)
- return HttpResponse('Error logged')
-
-
-class MimetypeTemplateView(TemplateView):
- """TemplateView with mimetype override"""
- template_name = "django_js_error_hook/utils.js"
- mimetype = "text/javascript"
-
- def render_to_response(self, context, **response_kwargs):
- """
- Before django 1.5 : 'mimetype'
- From django 1.5 : 'content_type'
-
- Add the parameter to return the right mimetype
- """
- if StrictVersion(get_version()) < StrictVersion('1.5'):
- mimetype_parameter = 'mimetype'
- else:
- mimetype_parameter = 'content_type'
-
- response_kwargs[mimetype_parameter] = self.mimetype
- return super(MimetypeTemplateView, self).render_to_response(context, **response_kwargs)
+ return HttpResponse("Error logged")
-utils_js = cache_page(2 * 31 * 24 * 60 * 60)(MimetypeTemplateView.as_view()) #: Cache 2 months
if CSRF_EXEMPT:
js_error_view = csrf_exempt(JSErrorHandlerView.as_view())
diff --git a/manage.py b/manage.py
index 9a7d44a..171bd6a 100644
--- a/manage.py
+++ b/manage.py
@@ -10,6 +10,6 @@
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
- "forget to activate a virtual environment?"
+ "forget to activate a virtual environment?",
) from exc
execute_from_command_line(sys.argv)
diff --git a/setup.py b/setup.py
index 4c38152..c2729f3 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
-# coding=utf-8
"""Python packaging."""
import os
+
from setuptools import setup
@@ -11,30 +11,31 @@ def read_relative_file(filename):
return f.read()
-NAME = 'django-js-error-hook'
-README = read_relative_file('README.rst')
-VERSION = read_relative_file('VERSION').strip()
-PACKAGES = ['django_js_error_hook']
-REQUIRES = ['django>=1.10']
+NAME = "django-js-error-hook"
+README = read_relative_file("README.rst")
+VERSION = read_relative_file("VERSION").strip()
+PACKAGES = ["django_js_error_hook"]
+REQUIRES = ["django>=3.2.0"]
-setup(name=NAME,
- version=VERSION,
- description='Generic handler for hooking client side javascript error.',
- long_description=README,
- classifiers=['Development Status :: 1 - Planning',
- 'License :: OSI Approved :: BSD License',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 2.7',
- 'Framework :: Django',
- ],
- keywords='class-based view, generic view, js error hooking',
- author='Jonathan Dorival',
- author_email='jonathan.dorival@novapost.fr',
- url='https://github.com/jojax/%s' % NAME,
- license='BSD',
- packages=PACKAGES,
- include_package_data=True,
- zip_safe=False,
- install_requires=REQUIRES,
- )
+setup(
+ name=NAME,
+ version=VERSION,
+ description="Generic handler for hooking client side javascript error.",
+ long_description=README,
+ classifiers=[
+ "Development Status :: 1 - Planning",
+ "License :: OSI Approved :: BSD License",
+ "Programming Language :: Python :: 3",
+ "Framework :: Django",
+ ],
+ keywords="class-based view, generic view, js error hooking",
+ author="Jonathan Dorival",
+ author_email="jonathan.dorival@novapost.fr",
+ url="https://github.com/jojax/%s" % NAME,
+ license="BSD",
+ packages=PACKAGES,
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=REQUIRES,
+)
diff --git a/test_settings.py b/test_settings.py
index 84fe59e..25a1372 100644
--- a/test_settings.py
+++ b/test_settings.py
@@ -1,6 +1,7 @@
# Django settings for demoproject project.
-from os.path import abspath, dirname, join
-demoproject_dir = dirname(abspath(__file__))
+from os.path import dirname
+
+demoproject_dir = dirname(__file__)
DEBUG = True
@@ -11,25 +12,25 @@
MANAGERS = ADMINS
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
- 'NAME': 'test.sqlite3', # Or path to database file if using sqlite3.
- 'USER': '', # Not used with sqlite3.
- 'PASSWORD': '', # Not used with sqlite3.
- 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
- 'PORT': '', # Set to empty string for default. Not used with sqlite3.
- }
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3", # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
+ "NAME": "test.sqlite3", # Or path to database file if using sqlite3.
+ "USER": "", # Not used with sqlite3.
+ "PASSWORD": "", # Not used with sqlite3.
+ "HOST": "", # Set to empty string for localhost. Not used with sqlite3.
+ "PORT": "", # Set to empty string for default. Not used with sqlite3.
+ },
}
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
-TIME_ZONE = 'America/Chicago'
+TIME_ZONE = "America/Chicago"
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
SITE_ID = 1
@@ -46,22 +47,22 @@
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
-MEDIA_ROOT = ''
+MEDIA_ROOT = ""
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
-MEDIA_URL = ''
+MEDIA_URL = ""
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
-STATIC_ROOT = ''
+STATIC_ROOT = ""
# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
# Additional locations of static files
STATICFILES_DIRS = (
@@ -73,44 +74,44 @@
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
- 'django.contrib.staticfiles.finders.FileSystemFinder',
- 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
-# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
+ "django.contrib.staticfiles.finders.FileSystemFinder",
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
+ # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody.
-SECRET_KEY = 'k9i055*z@6@9$7xvyw(8y4sk_w0@1ltf2$y-^zu^&wnlt1oez5'
+SECRET_KEY = "k9i055*z@6@9$7xvyw(8y4sk_w0@1ltf2$y-^zu^&wnlt1oez5"
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'APP_DIRS': True,
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "APP_DIRS": True,
},
]
MIDDLEWARE = (
- 'django.middleware.common.CommonMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
+ "django.middleware.common.CommonMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
-ROOT_URLCONF = 'django_js_error_hook.urls'
+ROOT_URLCONF = "django_js_error_hook.urls"
# Python dotted path to the WSGI application used by Django's runserver.
-WSGI_APPLICATION = 'demoproject.wsgi.application'
+WSGI_APPLICATION = "demoproject.wsgi.application"
INSTALLED_APPS = (
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.sites',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'django_js_error_hook',
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.sites",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "django_js_error_hook",
# Uncomment the next line to enable the admin:
# 'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
@@ -123,43 +124,37 @@
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'filters': {
- 'require_debug_false': {
- '()': 'django.utils.log.RequireDebugFalse'
- }
- },
- 'formatters': {
- 'verbose': {
- 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
- },
- 'simple': {
- 'format': '\033[22;32m%(levelname)s\033[0;0m %(message)s'
+ "version": 1,
+ "disable_existing_loggers": False,
+ "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
+ "formatters": {
+ "verbose": {
+ "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s",
},
+ "simple": {"format": "\033[22;32m%(levelname)s\033[0;0m %(message)s"},
},
- 'handlers': {
- 'mail_admins': {
- 'level': 'ERROR',
- 'filters': ['require_debug_false'],
- 'class': 'django.utils.log.AdminEmailHandler'
+ "handlers": {
+ "mail_admins": {
+ "level": "ERROR",
+ "filters": ["require_debug_false"],
+ "class": "django.utils.log.AdminEmailHandler",
+ },
+ "console": {
+ "level": "DEBUG",
+ "class": "logging.StreamHandler",
+ "formatter": "simple",
},
- 'console': {
- 'level': 'DEBUG',
- 'class': 'logging.StreamHandler',
- 'formatter': 'simple'
- },
},
- 'loggers': {
- 'django.request': {
- 'handlers': ['mail_admins'],
- 'level': 'ERROR',
- 'propagate': True,
+ "loggers": {
+ "django.request": {
+ "handlers": ["mail_admins"],
+ "level": "ERROR",
+ "propagate": True,
},
- 'javascript_error': {
- 'handlers': ['mail_admins', 'console'],
- 'level': 'ERROR',
- 'propagate': True,
+ "javascript_error": {
+ "handlers": ["mail_admins", "console"],
+ "level": "ERROR",
+ "propagate": True,
},
- }
+ },
}
diff --git a/tox.ini b/tox.ini
index 2fcb234..76cf7de 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,19 +1,21 @@
[tox]
envlist =
- py35-django110,
- {py35,py36,py37}-django111,
- {py35,py36,py37}-django20,
+ {py36,py37,py38,py39,py310}-django32,
+ {py38,py39,py310}-django40,
+ {py38,py39,py310}-django41,
[testenv]
deps=
- django110: Django>=1.10,<1.11
- django111: Django>=1.11,<2.0
- django20: Django>=2.0,<2.1
+ django32: Django>=3.2.0,<4.0.0
+ django40: Django>=4.0.0,<4.1.0
+ django41: Django>=4.1.0,<4.2.0
commands= python manage.py test --settings=test_settings
[gh-actions]
python =
- 3.5: py35
3.6: py36
3.7: py37
+ 3.8: py38
+ 3.9: py39
+ 3.10: py310