diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6b1bba3 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: sonus21 diff --git a/.gitignore b/.gitignore index 250135d..6c67d66 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,4 @@ venv.bak/ # mypy .mypy_cache/ .idea +.vscode/settings.json diff --git a/.travis.yml b/.travis.yml index 77ea768..93b2358 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ python: - 3.6 - 3.7 - 3.8 - - pypy - - pypy3 + # - pypy + # - pypy3 cache: pip install: - pip install -r requirements-dev.txt diff --git a/LICENSE.txt b/LICENSE.txt index 2941e08..8d53443 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright 2019 Sonu Kumar +Copyright 2019-2023 Sonu Kumar Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/README.rst b/README.rst index 7aa0dfd..4609943 100644 --- a/README.rst +++ b/README.rst @@ -182,7 +182,7 @@ Tests To run the tests, from the project directory, simply:: pip install -r requirements-dev.txt - bash tests/run-tests.sh + bash run-tests.sh You should see output similar to:: diff --git a/docs/source/conf.py b/docs/source/conf.py index 7b82e07..874f539 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,20 +14,21 @@ # import os import sys -sys.path.insert(0, os.path.abspath('../error_tracker')) +import datetime +sys.path.insert(0, os.path.abspath('../..')) +import error_tracker # -- Project information ----------------------------------------------------- project = u'error-tracker' -copyright = u'2019, Sonu Kumar' -author = u'Sonu Kumar' +copyright = str(datetime.datetime.now().year) + u', Sonu Kumar' +author = error_tracker.__author__ # The short X.Y version version = u'' # The full version, including alpha/beta/rc tags -release = u'1.0' - +release = error_tracker.__version__ # -- General configuration --------------------------------------------------- @@ -74,7 +75,6 @@ # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' - # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -109,7 +109,6 @@ # Output file base name for HTML help builder. htmlhelp_basename = 'error-tracker-doc' - # -- Options for LaTeX output ------------------------------------------------ latex_elements = { @@ -138,7 +137,6 @@ u'Sonu Kumar', 'manual'), ] - # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples @@ -148,7 +146,6 @@ [author], 1) ] - # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples @@ -160,10 +157,9 @@ 'Miscellaneous'), ] - # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} \ No newline at end of file +intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/docs/source/django-app.rst b/docs/source/django-app.rst index 5665634..2df24e9 100644 --- a/docs/source/django-app.rst +++ b/docs/source/django-app.rst @@ -106,6 +106,17 @@ Setting details Class must not have any constructor parameters and should implement __call__ method. +- Admin site support. + By default this is False, it should be used when default model is used, for custom model you should registered yourself. + + + .. code :: + + APP_ERROR_USE_DJANGO_ADMIN_SITE = True + + + + Manual Exception Tracking ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/images/admin1.png b/docs/source/images/admin1.png new file mode 100644 index 0000000..40e2fa7 Binary files /dev/null and b/docs/source/images/admin1.png differ diff --git a/docs/source/images/admin2.png b/docs/source/images/admin2.png new file mode 100644 index 0000000..59abc82 Binary files /dev/null and b/docs/source/images/admin2.png differ diff --git a/docs/source/images/detail-page.png b/docs/source/images/detail-page.png index f72ec46..c56bd6e 100644 Binary files a/docs/source/images/detail-page.png and b/docs/source/images/detail-page.png differ diff --git a/docs/source/images/home.png b/docs/source/images/home.png index 03db8cf..c4c2a22 100644 Binary files a/docs/source/images/home.png and b/docs/source/images/home.png differ diff --git a/docs/source/index.rst b/docs/source/index.rst index 53c876b..3de7362 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,6 +24,15 @@ Features .. image:: images/detail-page.png :alt: Detailed Exception page +**Admin dashboard** + +.. image:: images/admin1.png + :alt: Admin dashboard + + +.. image:: images/admin2.png + :alt: Admin dashboard + Quick start @@ -141,6 +150,7 @@ We need to update settings.py file as - Add Middleware :code:`error_tracker.django.middleware.ExceptionTrackerMiddleWare` for exception tracking [1]_. - Other configs related to notification - Add URLs to the list of URL patterns +- Enable django admin site (optional). .. [1] This should be added at the end so that it can process exception 1st in the middleware call stack. @@ -179,6 +189,12 @@ We need to add URLs to the urls.py so that we can browse the default pages provi url("dev/", include(urls)), ] +To enable the error tracker in the admin site add this line in your settings. + +.. code:: + + APP_ERROR_USE_DJANGO_ADMIN_SITE = True + Using With Python App (NO WEB SERVER) ------------------------------------- Choose either of the preferred framework, flask or Django and configure the app as per their specifications. diff --git a/error_tracker/__init__.py b/error_tracker/__init__.py index db06f35..5a72780 100644 --- a/error_tracker/__init__.py +++ b/error_tracker/__init__.py @@ -2,22 +2,42 @@ # # Error Tracking app # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # -__version__ = '1.1.6' +__version__ = '3.1.1' __author__ = 'Sonu Kumar' __email__ = 'sonunitw12@gmail.com' from error_tracker.libs.mixins import * -from error_tracker.flask import * -from error_tracker.django import * -from error_tracker.flask.utils import configure_scope as flask_scope -from error_tracker.django.apps import DjangoErrorTracker -from error_tracker.django.utils import capture_message, track_exception, configure_scope, capture_exception from error_tracker.libs.exception_formatter import * +flaskInstalled = False +try: + import flask + + flaskInstalled = True +except ImportError: + pass + +if flaskInstalled: + from error_tracker.flask import * + from error_tracker.flask.utils import configure_scope as flask_scope + +djangoInstalled = False +try: + import django + + djangoInstalled = True +except ImportError as e: + pass + +if djangoInstalled: + from error_tracker.django import * + from error_tracker.django.apps import DjangoErrorTracker + from error_tracker.django.utils import capture_message, track_exception, configure_scope, capture_exception + __all__ = [ # flask modules "AppErrorTracker", "DefaultFlaskContextBuilder", "flask_scope", diff --git a/error_tracker/django/__init__.py b/error_tracker/django/__init__.py index 93d4cf5..0addf62 100644 --- a/error_tracker/django/__init__.py +++ b/error_tracker/django/__init__.py @@ -2,7 +2,7 @@ # # Django components # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # @@ -40,11 +40,10 @@ def get_exception_model(): warning_message="Model " + model_path + " is not importable") if model is not None: return model - raise warnings.warn( + warnings.warn( "APP_ERROR_DB_MODEL refers to model '%s' that has not been installed" % model_path ) - warnings.warn("APP_ERROR_DB_MODEL is set but not usable using default model") - return ErrorModel + raise LookupError("APP_ERROR_DB_MODEL is set to '%s' but it's not importable" % model_path) def get_masking_module(): diff --git a/error_tracker/django/admin.py b/error_tracker/django/admin.py new file mode 100644 index 0000000..7e5722e --- /dev/null +++ b/error_tracker/django/admin.py @@ -0,0 +1,37 @@ +from .models import ErrorModel +from django.contrib import admin +from .settings import APP_ERROR_USE_DJANGO_ADMIN_SITE + +if APP_ERROR_USE_DJANGO_ADMIN_SITE: + @admin.register(ErrorModel) + class ErrorModelAdmin(admin.ModelAdmin): + def has_add_permission(self, request): + return False + + date_hierarchy = 'last_seen' + list_display = ( + 'host', + 'path', + 'method', + 'exception_name', + 'count', + 'created_on', + 'last_seen', + 'notification_sent', + 'ticket_raised', + ) + list_filter = ( + 'host', + 'notification_sent', + 'ticket_raised', + ) + search_fields = ('host', 'path', 'exception_name',) + change_form_template = 'error_tracker/admin/change_form.html' + + def changeform_view(self, request, object_id=None, form_url='', extra_context=None): + if object_id: + extra_context = { + 'obj': ErrorModel.objects.get(pk=object_id) + } + return super(ErrorModelAdmin, self).changeform_view(request, object_id=object_id, + form_url=form_url, extra_context=extra_context) diff --git a/error_tracker/django/middleware.py b/error_tracker/django/middleware.py index cf715db..f58b81d 100644 --- a/error_tracker/django/middleware.py +++ b/error_tracker/django/middleware.py @@ -2,13 +2,14 @@ # # Django error tracker middleware responsible for recording exception # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # from error_tracker.django import get_masking_module, get_context_builder, get_ticketing_module, \ get_exception_model, get_notification_module, APP_ERROR_SUBJECT_PREFIX, APP_ERROR_EMAIL_SENDER, \ - APP_ERROR_RECIPIENT_EMAIL, TRACK_ALL_EXCEPTIONS + APP_ERROR_RECIPIENT_EMAIL, TRACK_ALL_EXCEPTIONS, APP_ERROR_NOTIFICATION_ONCE, \ + APP_ERROR_TICKET_ONCE from error_tracker.libs.utils import get_exception_name, get_context_detail, get_notification_subject model = get_exception_model() @@ -60,13 +61,25 @@ def _raise_ticket(request, error): @staticmethod def _post_process(request, frame_str, frames, error): + send_notification = True + raise_ticket = True + if request is not None: message = ('URL: %s' % request.path) + '\n\n' else: message = "" message += frame_str - ErrorTracker._send_notification(request, message, frames[-1][:-1], error) - ErrorTracker._raise_ticket(request, error) + + if APP_ERROR_NOTIFICATION_ONCE is True and error.notification_sent is True: + send_notification = False + + if APP_ERROR_TICKET_ONCE is True and error.ticket_raised is True: + raise_ticket = False + + if send_notification: + ErrorTracker._send_notification(request, message, frames[-1][:-1], error) + if raise_ticket: + ErrorTracker._raise_ticket(request, error) def capture_exception(self, request=None, exception=None, additional_context=None): """ diff --git a/error_tracker/django/migrations/0002_auto_20201018_1311.py b/error_tracker/django/migrations/0002_auto_20201018_1311.py new file mode 100644 index 0000000..3634abe --- /dev/null +++ b/error_tracker/django/migrations/0002_auto_20201018_1311.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.2 on 2020-10-18 11:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('error_tracker', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='errormodel', + name='notification_sent', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='errormodel', + name='ticket_raised', + field=models.BooleanField(default=False), + ), + ] \ No newline at end of file diff --git a/error_tracker/django/models.py b/error_tracker/django/models.py index 9e3e30a..04f3ef7 100644 --- a/error_tracker/django/models.py +++ b/error_tracker/django/models.py @@ -2,7 +2,7 @@ # # Django error tracker default model # -# :copyright: 2019 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # from django.core.paginator import Paginator, EmptyPage @@ -16,9 +16,9 @@ Page = namedtuple("Page", "has_next, next_num, has_prev, prev_num, items ") -class ErrorModel(models.Model, ModelMixin): +class AbstractErrorModel(models.Model, ModelMixin): """ - Model to track exceptions + Base Model to track exceptions """ hash = models.CharField(max_length=64, primary_key=True) host = models.CharField(max_length=1024) @@ -30,10 +30,22 @@ class ErrorModel(models.Model, ModelMixin): count = models.IntegerField(default=0) created_on = models.DateTimeField(auto_now=True) last_seen = models.DateTimeField(auto_now=True, db_index=True) + notification_sent = models.BooleanField(default=False) + ticket_raised = models.BooleanField(default=False) @classmethod - def get_exceptions_per_page(cls, page_number=1): - records = cls.objects.all().order_by('last_seen') + def get_exceptions_per_page(cls, page_number=1, **query): + if query: + if 'page' in query: + page_number = query['page'] + del query['page'] + query = {"{}__icontains".format(k): v for k, v in query.items()} + + if not query: + records = cls.objects.all().order_by('last_seen') + else: + records = cls.objects.filter(**query).order_by('last_seen') + paginator = Paginator(records, EXCEPTION_APP_DEFAULT_LIST_SIZE) try: page = paginator.page(page_number) @@ -47,7 +59,7 @@ def get_exceptions_per_page(cls, page_number=1): @classmethod def get_entity(cls, rhash): - return cls.objects.get(pk=rhash) + return cls.objects.get(hash=rhash) @classmethod def create_or_update_entity(cls, rhash, host, path, method, request_data, exception_name, traceback): @@ -62,13 +74,24 @@ def create_or_update_entity(cls, rhash, host, path, method, request_data, except obj.count += 1 obj.last_seen = now() obj.save(update_fields=['count', 'last_seen']) + + return obj except Exception: print_exc() @classmethod def delete_entity(cls, rhash): - return cls.objects.filter(pk=rhash).delete() + return cls.objects.filter(hash=rhash).delete() class Meta: - db_table = 'exceptions' + abstract = True + + +class ErrorModel(AbstractErrorModel): + """ + Default Model to track exceptions + """ + + class Meta(AbstractErrorModel.Meta): swappable = 'APP_ERROR_DB_MODEL' + db_table = 'exceptions' diff --git a/error_tracker/django/settings.py b/error_tracker/django/settings.py index 167a952..76d693d 100644 --- a/error_tracker/django/settings.py +++ b/error_tracker/django/settings.py @@ -2,7 +2,7 @@ # # Django error tracker default settings # -# :copyright: 2020 Sonu Kumar +# :copyright: 2021 Sonu Kumar # :license: BSD-3-Clause # @@ -44,3 +44,9 @@ def get(key, default): APP_ERROR_DB_MODEL = get('APP_ERROR_DB_MODEL', None) # Check error views are visible to others or not APP_ERROR_VIEW_PERMISSION = get('APP_ERROR_VIEW_PERMISSION', None) +# Send email notification once +APP_ERROR_NOTIFICATION_ONCE = get('APP_ERROR_NOTIFICATION_ONCE', False) +# Raise ticket once +APP_ERROR_TICKET_ONCE = get('APP_ERROR_NOTIFICATION_ONCE', False) +# Use django admin site +APP_ERROR_USE_DJANGO_ADMIN_SITE = get('APP_ERROR_USE_DJANGO_ADMIN_SITE', False) diff --git a/error_tracker/django/templates/error_tracker/admin/change_form.html b/error_tracker/django/templates/error_tracker/admin/change_form.html new file mode 100644 index 0000000..8c30af3 --- /dev/null +++ b/error_tracker/django/templates/error_tracker/admin/change_form.html @@ -0,0 +1,6 @@ +{% extends 'admin/change_form.html' %} + + +{% block content %} +{% include 'error_tracker/detail.html' %} +{% endblock %} \ No newline at end of file diff --git a/error_tracker/django/templates/error_tracker/base.html b/error_tracker/django/templates/error_tracker/base.html index 387ec88..c8c9d99 100755 --- a/error_tracker/django/templates/error_tracker/base.html +++ b/error_tracker/django/templates/error_tracker/base.html @@ -1,17 +1,48 @@ +{% load i18n %} - - - - - - - -{{ title }} - -
- {% block content_block %} + + + + + + + {% block head_script %} + + + {% endblock %} -
- + + {{ title }} + +
+ {% block header_block %} +

+ {% trans 'Errors Seen' %} +

+ {% endblock %} + {% block content_block %} + {% endblock %} +
+ \ No newline at end of file diff --git a/error_tracker/django/templates/error_tracker/detail.html b/error_tracker/django/templates/error_tracker/detail.html index 2e3811a..961873a 100755 --- a/error_tracker/django/templates/error_tracker/detail.html +++ b/error_tracker/django/templates/error_tracker/detail.html @@ -1,36 +1,46 @@ {% extends 'error_tracker/base.html' %} -{% load error_tracker %} +{% load error_tracker i18n %} + {% block content_block %} - - {% if error %} -

{{ error }}

- {% else %} -

Errors Seen

-
-

URL:{{ obj.host }}{{ obj.path }}

-
-

Method: {{ obj.method }}

-
-

First time seen: {{ obj.created_on }}

-
-

Last seen: {{ obj.last_seen }}

-
-

Occurrences: {{ obj.count }}

-
-
Request data: {{ obj.request_data }}
-
-
Exception detail:
- {{ obj.traceback|escape|replace_new_line_with_br|safe }}
+
+
{%trans 'Method' %}
+
{%trans 'Referrer' %}
+
+ {{ obj.method }} +
+
+ {{ obj.host }}{{ obj.path }} +
+
+ +
+
+
{%trans 'First time seen' %}
+
{{ obj.created_on }}
+
+
+
{%trans 'Last seen' %}
+
{{ obj.last_seen }}
- {% endif %} +
+
{%trans 'Occurrences' %}
+
{{ obj.count }}
+
+
+
+
{%trans 'Request data' %}
+
{{obj.request_data|to_pretty}}
+
+
+
{%trans 'Exception detail' %}
+
 {{obj.traceback|escape|replace_new_line_with_br|safe}}
+
+
+ + +{% endif %} {% endblock %} \ No newline at end of file diff --git a/error_tracker/django/templates/error_tracker/list.html b/error_tracker/django/templates/error_tracker/list.html index 73be054..f33f1ac 100755 --- a/error_tracker/django/templates/error_tracker/list.html +++ b/error_tracker/django/templates/error_tracker/list.html @@ -1,58 +1,107 @@ {% extends 'error_tracker/base.html' %} +{% load i18n %} + +{% block head_script %} +{{block.super}} + +{%endblock%} + {% block content_block %} +
{% if error %}

{{ error }}

{% else %} -

Errors Seen

- - - - - - - - - - - - - - {% for error in errors.items %} - - - - - - - - - - {% endfor %} - -
HostMethodPathExceptionLast seenOccurrencesAction
{{ error.host }}{{ error.method }} - - {{ error.path|truncatechars:30 }} - - {{ error.exception_name }}{{ error.last_seen }}{{ error.count }} - Delete -
+
+
+ + + + + + + + + + + + + + + + + + + + + {% include 'error_tracker/partials/partial_table.html' %} + + +
{% trans 'Host' %}{% trans 'Method' %}{% trans 'Path' %}{% trans 'Exception' %}{% trans 'Last seen' %}#{% trans 'Action' %}
{% trans 'Reset' %}
+
+ {% if prev_url or next_url %} -
-
-
- {% if prev_url %} - - Newer exceptions - - {% endif %} - {% if next_url %} - - Older exceptions - - {% endif %} -
+ {% endif %} +
{% endif %} +
{% endblock %} \ No newline at end of file diff --git a/error_tracker/django/templates/error_tracker/partials/navigation.html b/error_tracker/django/templates/error_tracker/partials/navigation.html new file mode 100644 index 0000000..249d9a2 --- /dev/null +++ b/error_tracker/django/templates/error_tracker/partials/navigation.html @@ -0,0 +1,14 @@ +{% load i18n %} + +{% if prev_url %} + + {% trans 'Newer exceptions' %} +{% endif %} +{% if next_url %} + + {% trans 'Older exceptions' %} + +{% endif %} + + + diff --git a/error_tracker/django/templates/error_tracker/partials/partial_table.html b/error_tracker/django/templates/error_tracker/partials/partial_table.html new file mode 100644 index 0000000..3388f9b --- /dev/null +++ b/error_tracker/django/templates/error_tracker/partials/partial_table.html @@ -0,0 +1,19 @@ +{% load i18n %} + {% for error in errors.items %} + + {{ error.host }} + {{ error.method }} + + + {{ error.path|truncatechars:30 }} + + + {{ error.exception_name }} + {{ error.last_seen }} + {{ error.count }} + + {%trans 'Delete' %} + + + {% endfor %} diff --git a/error_tracker/django/templatetags/error_tracker.py b/error_tracker/django/templatetags/error_tracker.py index 9f75560..705854c 100644 --- a/error_tracker/django/templatetags/error_tracker.py +++ b/error_tracker/django/templatetags/error_tracker.py @@ -7,6 +7,8 @@ # from django import template +import json +from django.utils.safestring import mark_safe register = template.Library() @@ -14,3 +16,25 @@ @register.filter def replace_new_line_with_br(value): return value.replace("\n", "
") + + +@register.filter("to_pretty") +def to_pretty(x): + html = x + try: + x = json.loads(x) + except Exception as e: + try: + x = x.replace("'", '"').replace("\\\\", "\\") + x = json.loads(x) + except Exception as e: + pass + pass + + try: + html = "
{}
".format( + json.dumps(x, indent=4, sort_keys=True)) + except Exception as e: + pass + + return mark_safe(html) diff --git a/error_tracker/django/urls.py b/error_tracker/django/urls.py index 85bc7be..e1b3047 100755 --- a/error_tracker/django/urls.py +++ b/error_tracker/django/urls.py @@ -6,11 +6,12 @@ # :license: BSD-3-Clause # -from django.conf.urls import url +from django.urls import path from .views import detail, view_list, delete_exception +app_name = 'error_tracker' urlpatterns = [ - url(r'^$', view_list, name="view_errors"), - url(r'^(?P[\w-]+)/delete$', delete_exception, name='delete_error'), - url(r'^(?P[\w-]+)$', detail, name='view_error'), + path('', view_list, name="view_errors"), + path('/delete', delete_exception, name='delete_error'), + path('', detail, name='view_error'), ] diff --git a/error_tracker/django/utils.py b/error_tracker/django/utils.py index 4bc7430..99d54cf 100644 --- a/error_tracker/django/utils.py +++ b/error_tracker/django/utils.py @@ -2,18 +2,18 @@ # # Django error tracker utils classes # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # -import re import json - -from django.http import RawPostDataException +import re from error_tracker.libs.mixins import ContextBuilderMixin, NotificationMixin, ViewPermissionMixin from error_tracker.libs.utils import get_context_dict + from django.core.mail import send_mail +from django.http import RawPostDataException class DefaultDjangoContextBuilder(ContextBuilderMixin): @@ -54,14 +54,7 @@ def _get_form_data(request): @staticmethod def _get_headers(request): - if request is not None: - try: - headers = request.headers.dict() - except AttributeError: - regex = re.compile('^HTTP_') - headers = dict((regex.sub('', header), value) for (header, value) - in request.META.items() if header.startswith('HTTP_')) - return headers + return _HeaderExtractor(request).get_headers() @staticmethod def _get_args(request): @@ -88,6 +81,8 @@ def notify(self, request, exception, recipient_list=None): if recipient_list is not None and from_email is not None: send_mail(email_subject, email_body, from_email, recipient_list, fail_silently=True) + exception.notification_sent = True + exception.save() class DefaultDjangoViewPermission(ViewPermissionMixin): @@ -182,3 +177,59 @@ def capture_exception(request=None, exception=None, additional_context=None): from error_tracker.django.middleware import error_tracker error_tracker.capture_exception(request=request, exception=exception, additional_context=additional_context) + + +def clean_value(x): + x = x.value.replace('[["', "").replace('"]]', "").replace('"', "") + return x + + +class _HeaderExtractor(object): + ignored_keys = frozenset(['sec_ch_ua']) + regex = re.compile('^HTTP_') + + def __init__(self, request): + self.request = request + + def _get_raw_headers(self): + if self.request is None: + return {} + try: + return self.request.headers + except AttributeError: + return dict((self.regex.sub('', header), value) for (header, value) + in self.request.META.items() if header.startswith('HTTP_')) + + def _can_be_skipped(self, header_name, header_value): + return header_name.lower() in self.ignored_keys + + def get_headers(self): + headers = self._get_raw_headers() + new_headers = {} + for key, value in headers.items(): + if self._can_be_skipped(key, value): + continue + new_headers[key] = self.get_value(key, value) + return new_headers + + @staticmethod + def get_value(key, value): + try: + # Parse each key, value from headers items and Test if could be "json loaded". + # If not, we set the correspondent value to empty except for cookie key. + json.loads('{"%s":"%s"}' % (key, value)) + except Exception as e: + if key.lower() == "cookie": + try: + from http.cookies import SimpleCookie + try: + cookie = SimpleCookie() + cookie.load(value) + value = {k: clean_value(v) for k, v in cookie.items()} + except Exception as e: + value = "" + except ImportError: + pass + else: + value = "" + return value diff --git a/error_tracker/django/views.py b/error_tracker/django/views.py index 73b8c7a..97a6f7c 100644 --- a/error_tracker/django/views.py +++ b/error_tracker/django/views.py @@ -2,14 +2,16 @@ # # Django error tracker default value # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # -from django.http import Http404, HttpResponse +from django.http import Http404, HttpResponse, JsonResponse from django.shortcuts import redirect, render from django.urls import reverse from django.views.decorators.http import require_GET from error_tracker.django import get_exception_model, get_view_permission +from django.contrib.auth.decorators import login_required +from django.template.loader import render_to_string model = get_exception_model() @@ -34,17 +36,31 @@ def view_list(request): :return: rendered template """ title = "App Error" - try: - page = int(request.GET.get('page', 1)) - except: - page = 1 + + query = request.GET.dict() + error = False - errors = model.get_exceptions_per_page(page_number=page) - next_url = reverse('view_errors') + "?page=" + str(errors.next_num) \ + errors = model.get_exceptions_per_page(**query) + + next_url = reverse('error_tracker:view_errors') + "?page=" + str(errors.next_num) \ if errors.has_next else None - prev_url = reverse('view_errors') + "?page=" + str(errors.prev_num) \ + + prev_url = reverse('error_tracker:view_errors') + "?page=" + str(errors.prev_num) \ if errors.has_prev else None + is_ajax = request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' + if is_ajax or request.GET.get('ajax_partial'): + table = render_to_string('error_tracker/partials/partial_table.html', { + 'errors': errors, + }) + + navigation = render_to_string('error_tracker/partials/navigation.html', { + 'next_url': next_url, + 'prev_url': prev_url + }) + + return JsonResponse({'table': table, 'navigation': navigation}) + return render(request, template_name='error_tracker/list.html', context=dict(error=error, title=title, errors=errors, next_url=next_url, prev_url=prev_url)) @@ -60,7 +76,7 @@ def delete_exception(request, rhash): :return: redirect back to home page """ model.delete_entity(rhash) - return redirect(reverse('view_errors')) + return redirect(reverse('error_tracker:view_errors')) @require_GET diff --git a/error_tracker/flask/flask_error.py b/error_tracker/flask/flask_error.py index 9d7f5c7..805a651 100755 --- a/error_tracker/flask/flask_error.py +++ b/error_tracker/flask/flask_error.py @@ -2,7 +2,7 @@ # # Error tracker's flask plugin, this class initialize it's internal state # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # @@ -215,7 +215,7 @@ def create_or_update_entity(cls, rhash, host, path, method, request_data, @classmethod def get_exceptions_per_page(cls, page_number=1): return cls.query.order_by(desc(cls.last_seen)).paginate( - page_number, page_size, False) + page=page_number, per_page=page_size, error_out=False) @classmethod def get_entity(cls, rhash): @@ -285,10 +285,11 @@ def capture_exception(self, additional_context=None): ty, frames, frame_str, traceback_str, rhash, request_data = \ get_context_detail(rq, self.masking, self.context_builder, additional_context) - error = self.model.create_or_update_entity(rhash, host, path, method, - str(request_data), - get_exception_name(ty), - traceback_str) + with self.app.app_context(): + error = self.model.create_or_update_entity(rhash, host, path, method, + str(request_data), + get_exception_name(ty), + traceback_str) self._post_process(rq, frame_str, frames, error) def auto_track_exception(self, func, additional_context=None, silent=False): @@ -329,7 +330,8 @@ def get_exceptions(self, page_number=1): :return: list of exception objects """ if self.model: - return self.model.get_exceptions_per_page(page_number=page_number).items + with self.app.app_context(): + return self.model.get_exceptions_per_page(page_number=page_number).items raise ConfigError def get_exception(self, rhash): @@ -339,7 +341,8 @@ def get_exception(self, rhash): :return: exception object """ if self.model: - return self.model.get_entity(rhash) + with self.app.app_context(): + return self.model.get_entity(rhash) raise ConfigError def delete_exception(self, rhash): @@ -349,12 +352,14 @@ def delete_exception(self, rhash): :return: whatever model returns """ if self.model: - return self.model.delete_entity(rhash) + with self.app.app_context(): + return self.model.delete_entity(rhash) raise ConfigError def create_or_update_exception(self, rhash, host, path, method, request_data, exception_name, traceback): if self.model: - return self.model.create_or_update_entity(rhash, host, path, method, request_data, - exception_name, traceback) + with self.app.app_context(): + return self.model.create_or_update_entity(rhash, host, path, method, request_data, + exception_name, traceback) raise ConfigError diff --git a/error_tracker/flask/utils.py b/error_tracker/flask/utils.py index ffc5e74..d9a9ada 100644 --- a/error_tracker/flask/utils.py +++ b/error_tracker/flask/utils.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Utils modules for flask plugin # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # from error_tracker import ContextBuilderMixin, ViewPermissionMixin diff --git a/error_tracker/flask/view.py b/error_tracker/flask/view.py index 2bcb3c2..e898dfc 100644 --- a/error_tracker/flask/view.py +++ b/error_tracker/flask/view.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Exception formatter defaults view for flask app # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # diff --git a/error_tracker/libs/exception_formatter.py b/error_tracker/libs/exception_formatter.py index 4c54416..76580c6 100755 --- a/error_tracker/libs/exception_formatter.py +++ b/error_tracker/libs/exception_formatter.py @@ -2,7 +2,7 @@ # # Exception formatter that captures frame details in string format. # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # @@ -37,7 +37,7 @@ def convert_if_possible(x): return "QueryDict({%s})", x.dict() except ImportError: pass - return None, None + return None, x def format_frame(x, max_elements, max_string, max_recursion, masking=None): @@ -168,8 +168,9 @@ def format_exception(tb, max_elements=1000, w(val) if not masked: try: - w(format_frame(value, max_elements, max_string, max_recursion, - masking=masking)) + formatted_val = format_frame(value, max_elements, max_string, max_recursion, + masking=masking) + w(formatted_val) except Exception: exc_class = sys.exc_info()[0] w("<%s raised while printing value>" % exc_class) diff --git a/error_tracker/libs/mixins.py b/error_tracker/libs/mixins.py index 6cebb12..dfaef03 100755 --- a/error_tracker/libs/mixins.py +++ b/error_tracker/libs/mixins.py @@ -2,7 +2,7 @@ # # Exception formatter mixin classes # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # @@ -39,12 +39,14 @@ class ModelMixin(object): count = None created_on = None last_seen = None + notification_sent = None + ticket_raised = None def __str__(self): return "'%s' '%s' %s" % (self.host, self.path, self.count) def __unicode__(self): - return self.__str__() + return "'%s' '%s' %s" % (self.host, self.path, self.count) def __repr__(self): return "ModelMixin(%s)" % self.__str__() @@ -76,7 +78,7 @@ def create_or_update_entity(cls, rhash, host, path, method, request_data, @classmethod @abc.abstractmethod - def get_exceptions_per_page(cls, page_number=1): + def get_exceptions_per_page(cls, page_number=1, **kwargs): """ An object having these properties, has_next, next_num, has_prev, prev_num and items diff --git a/error_tracker/libs/utils.py b/error_tracker/libs/utils.py index 3aceace..d1447f7 100644 --- a/error_tracker/libs/utils.py +++ b/error_tracker/libs/utils.py @@ -2,7 +2,7 @@ # # Exception formatter utils module # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # import sys diff --git a/examples/DjangoSample/DjangoSample/settings.py b/examples/DjangoSample/DjangoSample/settings.py index 9a3d9e9..f48e3c6 100644 --- a/examples/DjangoSample/DjangoSample/settings.py +++ b/examples/DjangoSample/DjangoSample/settings.py @@ -115,3 +115,5 @@ # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = '/static/' +APP_ERROR_DB_MODEL = "core.models.TestErrorModel" +APP_ERROR_USE_DJANGO_ADMIN_SITE = True diff --git a/examples/DjangoSample/core/models.py b/examples/DjangoSample/core/models.py index 5b38dd4..1b16d78 100644 --- a/examples/DjangoSample/core/models.py +++ b/examples/DjangoSample/core/models.py @@ -7,60 +7,11 @@ # Create your models here. from django.utils.timezone import now -from error_tracker import ModelMixin, EXCEPTION_APP_DEFAULT_LIST_SIZE +from error_tracker.django.models import AbstractErrorModel Page = namedtuple("Page", "has_next, next_num, has_prev, prev_num, items ") -class TestErrorModel(models.Model, ModelMixin): - hash = models.CharField(max_length=64, primary_key=True) - host = models.CharField(max_length=1024) - path = models.CharField(max_length=4096) - method = models.CharField(max_length=64) - request_data = models.TextField() - exception_name = models.CharField(max_length=256) - traceback = models.TextField() - count = models.IntegerField(default=0) - created_on = models.DateTimeField(auto_now=True) - last_seen = models.DateTimeField(auto_now=True, db_index=True) - - @classmethod - def get_exceptions_per_page(cls, page_number=1): - records = cls.objects.all().order_by('last_seen') - paginator = Paginator(records, EXCEPTION_APP_DEFAULT_LIST_SIZE) - try: - page = paginator.page(page_number) - return Page(page.has_next(), - page.next_page_number() if page.has_next() else None, - page.has_previous(), - page.previous_page_number() if page.has_previous() else None, - page.object_list) - except EmptyPage: - return Page(False, None, True, paginator.num_pages, []) - - @classmethod - def get_entity(cls, rhash): - return cls.objects.get(pk=rhash) - - @classmethod - def create_or_update_entity(cls, rhash, host, path, method, request_data, exception_name, traceback): - try: - obj, created = cls.objects.get_or_create(hash=rhash) - if created: - obj.host, obj.path, obj.method, obj.request_data, obj.exception_name, obj.traceback = \ - host, path, method, request_data, exception_name, traceback - obj.count = 1 - obj.save() - else: - obj.count += 1 - obj.last_seen = now() - obj.save(update_fields=['count', 'last_seen']) - except Exception: - print_exc() - - @classmethod - def delete_entity(cls, rhash): - return cls.objects.filter(pk=rhash).delete() - - class Meta: +class TestErrorModel(AbstractErrorModel): + class Meta(AbstractErrorModel.Meta): db_table = 'test_exceptions' diff --git a/locale/fr_FR/LC_MESSAGES/django.po b/locale/fr_FR/LC_MESSAGES/django.po new file mode 100644 index 0000000..68a4807 --- /dev/null +++ b/locale/fr_FR/LC_MESSAGES/django.po @@ -0,0 +1,63 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , 2020. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-10-22 15:02+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Thierry BOULOGNE \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: error_tracker/django/apps.py:17 +msgid "Error Monitoring and Exception Tracking" +msgstr "" + +#: error_tracker/django/templates/error_tracker/list.html:86 +msgid "Host" +msgstr "Hôtesss" + +#: error_tracker/django/templates/error_tracker/list.html:87 +msgid "Method" +msgstr "Méthode" + +#: error_tracker/django/templates/error_tracker/list.html:88 +msgid "Path" +msgstr "Chemin" + +#: error_tracker/django/templates/error_tracker/list.html:89 +msgid "Exception" +msgstr "Erreur" + +#: error_tracker/django/templates/error_tracker/list.html:90 +msgid "Last seen" +msgstr "Dernière occurence" + +#: error_tracker/django/templates/error_tracker/list.html:92 +msgid "Action" +msgstr "Action" + +#: error_tracker/django/templates/error_tracker/list.html:101 +msgid "Reset" +msgstr "Effacer" + +#: error_tracker/django/templates/error_tracker/partials/navigation.html:5 +msgid "Newer exceptions" +msgstr "Suivant" + +#: error_tracker/django/templates/error_tracker/partials/navigation.html:9 +msgid "Older exceptions" +msgstr "Précédent" + +#: error_tracker/django/templates/error_tracker/partials/partial_table.html:16 +msgid "Delete" +msgstr "Supprimer" diff --git a/requirements-dev.txt b/requirements-dev.txt index 9af00a5..3268bf6 100755 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ Flask Flask-Mail Flask-SQLAlchemy -pyquery +pyquery==1.4.1 Django six coveralls diff --git a/requirements.txt b/requirements.txt deleted file mode 100755 index ab058f3..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -six -Flask -Flask-SQLAlchemy -Django -djangorestframework \ No newline at end of file diff --git a/setup.py b/setup.py index 0347617..42744ab 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ def grep(attrname): }, tests_require=[ "Flask-Mail", - 'pyquery' + 'pyquery', "Django", "djangorestframework", "Flask", @@ -69,6 +69,8 @@ def grep(attrname): 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Topic :: Software Development :: Libraries :: Python Modules', ] ) diff --git a/tests/DjangoTest/DjangoTest/drf_urls.py b/tests/DjangoTest/DjangoTest/drf_urls.py index 939c976..2542732 100644 --- a/tests/DjangoTest/DjangoTest/drf_urls.py +++ b/tests/DjangoTest/DjangoTest/drf_urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url, include +from django.urls import path, include from django.contrib import admin from rest_framework import routers from error_tracker.django import urls @@ -9,11 +9,11 @@ router.register(r'users', UserViewSet) urlpatterns = [ - url('admin/', admin.site.urls), - url("dev/", include(urls)), - url(r'^$', views.index), - url(r'^value-error$', views.value_error), - url(r'^post-view$', views.post_view), - url(r'^', include(router.urls)), - url(r'^api-auth/', include('rest_framework.urls')) + path('admin/', admin.site.urls), + path('dev/', include(urls)), + path('', views.index), + path('value-error', views.value_error), + path('post-view', views.post_view), + path('', include(router.urls)), + path('api-auth/', include('rest_framework.urls')) ] diff --git a/tests/DjangoTest/DjangoTest/urls.py b/tests/DjangoTest/DjangoTest/urls.py index e22991c..b85bf2a 100644 --- a/tests/DjangoTest/DjangoTest/urls.py +++ b/tests/DjangoTest/DjangoTest/urls.py @@ -13,15 +13,15 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.conf.urls import url, include +from django.urls import path, include from django.contrib import admin from error_tracker.django import urls from core import views urlpatterns = [ - url('admin/', admin.site.urls), - url("dev/", include(urls)), - url(r'^$', views.index), - url(r'^value-error$', views.value_error), - url(r'^post-view$', views.post_view), + path('admin/', admin.site.urls), + path('dev/', include(urls)), + path('', views.index), + path('value-error', views.value_error), + path('post-view', views.post_view), ] diff --git a/tests/DjangoTest/tests/test_401_end_point.py b/tests/DjangoTest/tests/test_401_end_point.py index 2c2184e..077e253 100644 --- a/tests/DjangoTest/tests/test_401_end_point.py +++ b/tests/DjangoTest/tests/test_401_end_point.py @@ -7,7 +7,7 @@ # # Test view permission feature # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # diff --git a/tests/DjangoTest/tests/test_drf.py b/tests/DjangoTest/tests/test_drf.py index 6514249..17357ec 100644 --- a/tests/DjangoTest/tests/test_drf.py +++ b/tests/DjangoTest/tests/test_drf.py @@ -2,7 +2,7 @@ # # Test Django Rest framework related changes # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # diff --git a/tests/DjangoTest/tests/test_end_point.py b/tests/DjangoTest/tests/test_end_point.py index 6a51682..8cf9a11 100644 --- a/tests/DjangoTest/tests/test_end_point.py +++ b/tests/DjangoTest/tests/test_end_point.py @@ -2,7 +2,7 @@ # # Test all end points are working as expected # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # @@ -20,23 +20,22 @@ def test_list_view(self): self.get('/value-error') self.post('/post-view') html = self.get('/dev', follow=True).content - urls = [node.attrib['href'] for node in pyquery.PyQuery(html)('a')] - # 2 links for delete operation and 2 links to navigate - self.assertEqual(len(urls), 2 + 2) + urls = [node.attrib['href'] for node in pyquery.PyQuery(html)('a.view-link, a.home-link, a.delete')] + # 2 links for delete operation and 2 links to navigate and 1 link to home page + self.assertEqual(len(urls), 2 + 3) - urls = [node.attrib['href'] for node in pyquery.PyQuery(html)('.view-link')] + urls = [node.attrib['href'] for node in pyquery.PyQuery(html)('a.view-link')] self.assertEqual(len(urls), 2) def test_detail_view(self): self.get('/value-error') html = self.get('/dev', follow=True).content - url = [node.attrib['href'] for node in pyquery.PyQuery(html)('.view-link')][0] + url = [node.attrib['href'] for node in pyquery.PyQuery(html)('a.view-link')][0] response = self.get(url).content - row = pyquery.PyQuery(response)('.row')[0] - p = pyquery.PyQuery(row)('p') - divs = pyquery.PyQuery(row)('.row div') - self.assertEqual(len(p), 5) - self.assertEqual(len(divs), 2) + row = pyquery.PyQuery(response)('.mb-4') + self.assertEqual(2, len(row)) + divs = pyquery.PyQuery(response)('.row>div') + self.assertEqual(len(divs), 11) def test_delete_view(self): self.get('/value-error') @@ -61,24 +60,24 @@ def test_pagination(self): exception.traceback) response = self.get('/dev', follow=True).content - urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a')] - self.assertEqual(len(urls), settings.EXCEPTION_APP_DEFAULT_LIST_SIZE * 2 + 1) + urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a.view-link, a.delete, a.pagelink, a.home-link')] + self.assertEqual(len(urls), settings.EXCEPTION_APP_DEFAULT_LIST_SIZE * 2 + 2) self.assertTrue('/dev/?page=2' in urls) response = self.get('/dev/?page=2', follow=True).content - urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a')] - self.assertEqual(len(urls), settings.EXCEPTION_APP_DEFAULT_LIST_SIZE * 2 + 2) + urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a.view-link, a.delete, a.pagelink, a.home-link')] + self.assertEqual(len(urls), settings.EXCEPTION_APP_DEFAULT_LIST_SIZE * 2 + 3) self.assertTrue('/dev/?page=1' in urls) self.assertTrue('/dev/?page=3' in urls) response = self.get('/dev/?page=5', follow=True).content - urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a')] + urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a.view-link, a.delete, a.pagelink, a.home-link')] self.assertTrue('/dev/?page=4' in urls) response = self.get('/dev/?page=6', follow=True).content - urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a')] - self.assertEqual(len(urls), 1) + urls = [node.attrib['href'] for node in pyquery.PyQuery(response)('a.view-link, a.delete, a.pagelink, a.home-link')] + self.assertEqual(len(urls), 2) if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/tests/DjangoTest/tests/test_manual_error_tracking.py b/tests/DjangoTest/tests/test_manual_error_tracking.py index 8b5d07c..050bee2 100644 --- a/tests/DjangoTest/tests/test_manual_error_tracking.py +++ b/tests/DjangoTest/tests/test_manual_error_tracking.py @@ -2,7 +2,7 @@ # # Basic test case test, this tests basic part of application # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # import unittest diff --git a/tests/DjangoTest/tests/test_no_masking.py b/tests/DjangoTest/tests/test_no_masking.py index 7a6f2be..1d617cb 100644 --- a/tests/DjangoTest/tests/test_no_masking.py +++ b/tests/DjangoTest/tests/test_no_masking.py @@ -2,7 +2,7 @@ # # Test no masking feature # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # @@ -15,15 +15,18 @@ class NoMasking(TestBase, TestCase): def test_no_mask(self): - self.post('/post-view', data=dict( - username="username", - password="password")) + self.post('/post-view', data=dict(username="username", password="password")) errors = self.get_exceptions() error = errors[0] re1 = r".*password.* = .*" re2 = r".*secret.* = .*" + re3 = r".*form.* = .*" + re4 = r'.*l.* = \[.*\]' re1 = re.compile(re1, re.IGNORECASE) re2 = re.compile(re2, re.IGNORECASE) + re3 = re.compile(re3, re.IGNORECASE) + re4 = re.compile(re4, re.IGNORECASE) + exception = error.traceback matches1 = re1.findall(exception) matches2 = re2.findall(exception) @@ -33,6 +36,13 @@ def test_no_mask(self): key, value = match.split(" = ") self.assertNotEqual(value, "%r" % APP_ERROR_MASK_WITH) + matches3 = re3.findall(exception) + matches4 = re4.findall(exception) + self.assertEqual(1, len(matches4)) + self.assertEqual(1, len(matches3)) + self.assertEqual("[1, 2, 3, 4]", matches4[0].strip().split("l = ")[1]) + data = matches3[0].strip().split("form = ")[1].split("QueryDict(")[1].split(')')[0] + self.assertEqual(True, data == "{'password' : 'password', 'username' : 'username'}") re3 = r".*password.* : .*" re3 = re.compile(re3, re.IGNORECASE) matches3 = re3.findall(exception) diff --git a/tests/DjangoTest/tests/util.py b/tests/DjangoTest/tests/util.py index c437a0a..b191c86 100644 --- a/tests/DjangoTest/tests/util.py +++ b/tests/DjangoTest/tests/util.py @@ -2,7 +2,7 @@ # # Test utils # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # diff --git a/tests/FlaskTest/test_401_views.py b/tests/FlaskTest/test_401_views.py index 96aa940..8ba4a49 100644 --- a/tests/FlaskTest/test_401_views.py +++ b/tests/FlaskTest/test_401_views.py @@ -2,7 +2,7 @@ # # Test view permission feature # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # diff --git a/tests/FlaskTest/test_db_model.py b/tests/FlaskTest/test_db_model.py index 6cfa064..996eeeb 100644 --- a/tests/FlaskTest/test_db_model.py +++ b/tests/FlaskTest/test_db_model.py @@ -2,7 +2,7 @@ # # Test custom model features # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # diff --git a/tests/FlaskTest/test_end_point.py b/tests/FlaskTest/test_end_point.py index 09c3224..42652aa 100644 --- a/tests/FlaskTest/test_end_point.py +++ b/tests/FlaskTest/test_end_point.py @@ -69,18 +69,19 @@ def test_pagination(self): exception = error_tracker.get_exceptions()[0] hashx = exception.hash inserted = 0 - i = 0 while inserted < 20: - i += 1 - idx = str(i) + hashx[2:] inserted += 1 + idx = str(inserted) + hashx[2:] + print("Inserting ", idx) error_tracker.create_or_update_exception(idx, exception.host, exception.path, exception.method, exception.request_data, exception.exception_name, exception.traceback) + response = c.get('/dev/error', follow_redirects=True) - urls = [node.attrib['href'] for node in pyquery.PyQuery(response.data)('a')] + data = response.data + urls = [node.attrib['href'] for node in pyquery.PyQuery(data)('a')] self.assertEqual(len(urls), pagination_config.APP_DEFAULT_LIST_SIZE * 2 + 1) self.assertTrue('/dev/error/?page=2' in urls) diff --git a/tests/FlaskTest/test_init_later.py b/tests/FlaskTest/test_init_later.py index 3390646..d01fb4d 100644 --- a/tests/FlaskTest/test_init_later.py +++ b/tests/FlaskTest/test_init_later.py @@ -2,7 +2,7 @@ # # Test app initialization post constructions # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # @@ -24,8 +24,9 @@ def _setup(self, db_file): db = SQLAlchemy(app) error_tracker = AppErrorTracker() error_tracker.init_app(app, db, view_permission=ViewPermission()) - db.drop_all() - db.create_all() + with app.app_context(): + db.drop_all() + db.create_all() return app, db, error_tracker diff --git a/tests/FlaskTest/test_manual_error_tracking.py b/tests/FlaskTest/test_manual_error_tracking.py index 988a2d9..668bebd 100644 --- a/tests/FlaskTest/test_manual_error_tracking.py +++ b/tests/FlaskTest/test_manual_error_tracking.py @@ -2,7 +2,7 @@ # # Test manual error tracking is working or not # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # import unittest diff --git a/tests/FlaskTest/utils.py b/tests/FlaskTest/utils.py index da81d20..9921bdb 100644 --- a/tests/FlaskTest/utils.py +++ b/tests/FlaskTest/utils.py @@ -2,7 +2,7 @@ # # Test's util class # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause # @@ -97,6 +97,7 @@ def _setup(self, db_file): app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///%s" % db_file db = SQLAlchemy(app) error_tracker = AppErrorTracker(app=app, db=db, view_permission=ViewPermission(), **self.kwargs) - db.drop_all() - db.create_all() + with app.app_context(): + db.drop_all() + db.create_all() return app, db, error_tracker diff --git a/tests/utils.py b/tests/utils.py index 7979849..7e75884 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,7 +2,7 @@ # # Test utility # -# :copyright: 2020 Sonu Kumar +# :copyright: 2023 Sonu Kumar # :license: BSD-3-Clause #