exceptbot-drf 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exceptbot/__init__.py +1 -0
- exceptbot/admin.py +260 -0
- exceptbot/apps.py +7 -0
- exceptbot/middleware.py +126 -0
- exceptbot/migrations/0001_initial.py +63 -0
- exceptbot/migrations/__init__.py +0 -0
- exceptbot/models.py +134 -0
- exceptbot/permissions.py +11 -0
- exceptbot/serializers.py +65 -0
- exceptbot/tests.py +0 -0
- exceptbot/urls.py +85 -0
- exceptbot/views.py +272 -0
- exceptbot_drf-1.0.0.dist-info/METADATA +277 -0
- exceptbot_drf-1.0.0.dist-info/RECORD +17 -0
- exceptbot_drf-1.0.0.dist-info/WHEEL +5 -0
- exceptbot_drf-1.0.0.dist-info/licenses/LICENSE.md +49 -0
- exceptbot_drf-1.0.0.dist-info/top_level.txt +1 -0
exceptbot/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default_app_config = 'exceptbot.apps.ExceptBotConfig'
|
exceptbot/admin.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.utils import timezone
|
|
3
|
+
from django.utils.html import format_html
|
|
4
|
+
|
|
5
|
+
from .models import ExceptionLog, AppSettings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@admin.register(ExceptionLog)
|
|
9
|
+
class ExceptionLogAdmin(admin.ModelAdmin):
|
|
10
|
+
list_display = (
|
|
11
|
+
'id',
|
|
12
|
+
'exception_type',
|
|
13
|
+
'short_url',
|
|
14
|
+
'http_method',
|
|
15
|
+
'status_code',
|
|
16
|
+
'source_badge',
|
|
17
|
+
'file_name_short',
|
|
18
|
+
'line_number',
|
|
19
|
+
'user',
|
|
20
|
+
'count',
|
|
21
|
+
'is_resolved_badge',
|
|
22
|
+
'timestamp',
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
list_filter = (
|
|
26
|
+
'is_resolved',
|
|
27
|
+
'source',
|
|
28
|
+
'http_method',
|
|
29
|
+
'status_code',
|
|
30
|
+
'exception_type',
|
|
31
|
+
'timestamp',
|
|
32
|
+
'user',
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
search_fields = (
|
|
36
|
+
'exception_type',
|
|
37
|
+
'url_path',
|
|
38
|
+
'file_name',
|
|
39
|
+
'full_error_message',
|
|
40
|
+
'error_line_content',
|
|
41
|
+
'user__username',
|
|
42
|
+
'user__email',
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
readonly_fields = (
|
|
46
|
+
'url_path',
|
|
47
|
+
'http_method',
|
|
48
|
+
'status_code',
|
|
49
|
+
'source',
|
|
50
|
+
'request_data_display',
|
|
51
|
+
'ip_address',
|
|
52
|
+
'user_agent',
|
|
53
|
+
'exception_type',
|
|
54
|
+
'full_error_message_display',
|
|
55
|
+
'file_name',
|
|
56
|
+
'file_content_display',
|
|
57
|
+
'line_number',
|
|
58
|
+
'error_line_content',
|
|
59
|
+
'user',
|
|
60
|
+
'timestamp',
|
|
61
|
+
'count',
|
|
62
|
+
'resolved_at',
|
|
63
|
+
'ai_suggestion_display',
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
ordering = ('-timestamp',)
|
|
67
|
+
|
|
68
|
+
list_per_page = 25
|
|
69
|
+
|
|
70
|
+
fieldsets = (
|
|
71
|
+
('error info', {
|
|
72
|
+
'fields': (
|
|
73
|
+
'exception_type',
|
|
74
|
+
'url_path',
|
|
75
|
+
'http_method',
|
|
76
|
+
'status_code',
|
|
77
|
+
'source',
|
|
78
|
+
'timestamp',
|
|
79
|
+
'count',
|
|
80
|
+
)
|
|
81
|
+
}),
|
|
82
|
+
(' position of code', {
|
|
83
|
+
'fields': (
|
|
84
|
+
'file_name',
|
|
85
|
+
'line_number',
|
|
86
|
+
'error_line_content',
|
|
87
|
+
'file_content_display',
|
|
88
|
+
)
|
|
89
|
+
}),
|
|
90
|
+
(' request information', {
|
|
91
|
+
'fields': (
|
|
92
|
+
'request_data_display',
|
|
93
|
+
'ip_address',
|
|
94
|
+
'user_agent',
|
|
95
|
+
),
|
|
96
|
+
'classes': ('collapse',),
|
|
97
|
+
}),
|
|
98
|
+
(' error message', {
|
|
99
|
+
'fields': (
|
|
100
|
+
'full_error_message_display',
|
|
101
|
+
),
|
|
102
|
+
'classes': ('collapse',),
|
|
103
|
+
}),
|
|
104
|
+
('user and status resolved', {
|
|
105
|
+
'fields': (
|
|
106
|
+
'user',
|
|
107
|
+
'is_resolved',
|
|
108
|
+
'resolved_by',
|
|
109
|
+
'resolved_at',
|
|
110
|
+
'resolution_note',
|
|
111
|
+
)
|
|
112
|
+
}),
|
|
113
|
+
('suggestion of ai', {
|
|
114
|
+
'fields': (
|
|
115
|
+
'ai_suggestion_display',
|
|
116
|
+
),
|
|
117
|
+
'classes': ('collapse',),
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# actions = ['mark_as_resolved', 'mark_as_unresolved']
|
|
122
|
+
|
|
123
|
+
@admin.display(description='URL')
|
|
124
|
+
def short_url(self, obj):
|
|
125
|
+
if len(obj.url_path) > 40:
|
|
126
|
+
return obj.url_path[:40] + '...'
|
|
127
|
+
return obj.url_path
|
|
128
|
+
|
|
129
|
+
@admin.display(description='File')
|
|
130
|
+
def file_name_short(self, obj):
|
|
131
|
+
if not obj.file_name:
|
|
132
|
+
return '-'
|
|
133
|
+
return obj.file_name.split('/')[-1].split('\\')[-1]
|
|
134
|
+
|
|
135
|
+
@admin.display(description='Source')
|
|
136
|
+
def source_badge(self, obj):
|
|
137
|
+
colors = {
|
|
138
|
+
'backend': '#007bff',
|
|
139
|
+
'frontend': '#28a745',
|
|
140
|
+
'unknown': '#6c757d',
|
|
141
|
+
}
|
|
142
|
+
color = colors.get(obj.source, '#6c757d')
|
|
143
|
+
return format_html(
|
|
144
|
+
'<span style="background:{};color:white;padding:2px 8px;'
|
|
145
|
+
'border-radius:4px;font-size:11px;">{}</span>',
|
|
146
|
+
color,
|
|
147
|
+
obj.get_source_display(),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@admin.display(description='وضعیت', boolean=True)
|
|
151
|
+
def is_resolved_badge(self, obj):
|
|
152
|
+
return obj.is_resolved
|
|
153
|
+
|
|
154
|
+
@admin.display(description='Request Data')
|
|
155
|
+
def request_data_display(self, obj):
|
|
156
|
+
if not obj.request_data:
|
|
157
|
+
return '-'
|
|
158
|
+
import json
|
|
159
|
+
pretty = json.dumps(obj.request_data, indent=2, ensure_ascii=False)
|
|
160
|
+
return format_html(
|
|
161
|
+
'<pre style="background:#f8f9fa;padding:10px;'
|
|
162
|
+
'border-radius:4px;max-height:400px;overflow:auto;">{}</pre>',
|
|
163
|
+
pretty,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@admin.display(description='Full Error Message')
|
|
167
|
+
def full_error_message_display(self, obj):
|
|
168
|
+
if not obj.full_error_message:
|
|
169
|
+
return '-'
|
|
170
|
+
return format_html(
|
|
171
|
+
'<pre style="background:#fff3cd;padding:10px;'
|
|
172
|
+
'border-radius:4px;max-height:400px;overflow:auto;'
|
|
173
|
+
'white-space:pre-wrap;">{}</pre>',
|
|
174
|
+
obj.full_error_message,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
@admin.display(description='File Content')
|
|
178
|
+
def file_content_display(self, obj):
|
|
179
|
+
if not obj.file_content:
|
|
180
|
+
return '-'
|
|
181
|
+
return format_html(
|
|
182
|
+
'<pre style="background:#f8f9fa;padding:10px;'
|
|
183
|
+
'border-radius:4px;max-height:500px;overflow:auto;'
|
|
184
|
+
'white-space:pre-wrap;">{}</pre>',
|
|
185
|
+
obj.file_content,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@admin.display(description='AI Suggestion')
|
|
189
|
+
def ai_suggestion_display(self, obj):
|
|
190
|
+
if not obj.ai_suggestion:
|
|
191
|
+
return 'هنوز پیشنهادی ساخته نشده است.'
|
|
192
|
+
return format_html(
|
|
193
|
+
'<div style="background:#e7f3ff;padding:15px;'
|
|
194
|
+
'border-radius:4px;max-height:600px;overflow:auto;'
|
|
195
|
+
'white-space:pre-wrap;">{}</div>',
|
|
196
|
+
obj.ai_suggestion,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@admin.action(description='✅ علامتگذاری بهعنوان رفعشده')
|
|
200
|
+
def mark_as_resolved(self, request, queryset):
|
|
201
|
+
updated = queryset.update(
|
|
202
|
+
is_resolved=True,
|
|
203
|
+
resolved_by=request.user,
|
|
204
|
+
resolved_at=timezone.now(),
|
|
205
|
+
)
|
|
206
|
+
self.message_user(
|
|
207
|
+
request,
|
|
208
|
+
f'{updated} خطا بهعنوان رفعشده علامتگذاری شد.'
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@admin.action(description='↩️ برگرداندن به حالت رفعنشده')
|
|
212
|
+
def mark_as_unresolved(self, request, queryset):
|
|
213
|
+
updated = queryset.update(
|
|
214
|
+
is_resolved=False,
|
|
215
|
+
resolved_by=None,
|
|
216
|
+
resolved_at=None,
|
|
217
|
+
resolution_note='',
|
|
218
|
+
)
|
|
219
|
+
self.message_user(
|
|
220
|
+
request,
|
|
221
|
+
f'{updated} خطا به حالت رفعنشده برگشت.'
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def has_add_permission(self, request):
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
def has_change_permission(self, request, obj=None):
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@admin.register(AppSettings)
|
|
232
|
+
class AppSettingsAdmin(admin.ModelAdmin):
|
|
233
|
+
list_display = (
|
|
234
|
+
'id',
|
|
235
|
+
'project_name',
|
|
236
|
+
'base_url',
|
|
237
|
+
'has_openai_key',
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
fieldsets = (
|
|
241
|
+
('اطلاعات پروژه', {
|
|
242
|
+
'fields': ('project_name', 'base_url'),
|
|
243
|
+
}),
|
|
244
|
+
('تنظیمات هوش مصنوعی', {
|
|
245
|
+
'fields': ('openai_api_key',),
|
|
246
|
+
'description': 'برای دریافت پیشنهاد AI، کلید OpenAI خود را وارد کنید.',
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
@admin.display(description='OpenAI Key', boolean=True)
|
|
251
|
+
def has_openai_key(self, obj):
|
|
252
|
+
return bool(obj.openai_api_key)
|
|
253
|
+
|
|
254
|
+
def has_add_permission(self, request):
|
|
255
|
+
if AppSettings.objects.exists():
|
|
256
|
+
return False
|
|
257
|
+
return super().has_add_permission(request)
|
|
258
|
+
|
|
259
|
+
def has_delete_permission(self, request, obj=None):
|
|
260
|
+
return False
|
exceptbot/apps.py
ADDED
exceptbot/middleware.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
|
|
4
|
+
from django.db import transaction
|
|
5
|
+
from django.utils import timezone
|
|
6
|
+
from .models import ExceptionLog, AppSettings
|
|
7
|
+
SENSITIVE_FIELDS = {
|
|
8
|
+
'password', 'password1', 'password2',
|
|
9
|
+
'token', 'access', 'refresh',
|
|
10
|
+
'secret', 'api_key', 'authorization',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
def mask_sensitive_data(data):
|
|
14
|
+
if not isinstance(data, dict):
|
|
15
|
+
return data
|
|
16
|
+
|
|
17
|
+
masked = {}
|
|
18
|
+
for key, value in data.items():
|
|
19
|
+
if key.lower() in SENSITIVE_FIELDS:
|
|
20
|
+
masked[key] = '***MASKED***'
|
|
21
|
+
elif isinstance(value, dict):
|
|
22
|
+
masked[key] = mask_sensitive_data(value)
|
|
23
|
+
elif isinstance(value, list):
|
|
24
|
+
masked[key] = [
|
|
25
|
+
mask_sensitive_data(item) if isinstance(item, dict) else item
|
|
26
|
+
for item in value
|
|
27
|
+
]
|
|
28
|
+
else:
|
|
29
|
+
masked[key] = value
|
|
30
|
+
return masked
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ExceptBotMiddleware:
|
|
34
|
+
def __init__(self, get_response):
|
|
35
|
+
self.get_response = get_response
|
|
36
|
+
|
|
37
|
+
def __call__(self, request):
|
|
38
|
+
response = self.get_response(request)
|
|
39
|
+
return response
|
|
40
|
+
|
|
41
|
+
def process_exception(self, request, exception):
|
|
42
|
+
user = None
|
|
43
|
+
if hasattr(request, 'user') and request.user.is_authenticated:
|
|
44
|
+
user = request.user
|
|
45
|
+
app_settings = AppSettings.get_solo()
|
|
46
|
+
project_name = app_settings.project_name or ''
|
|
47
|
+
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
48
|
+
formatted_traceback = traceback.extract_tb(exc_traceback)
|
|
49
|
+
|
|
50
|
+
file_name = None
|
|
51
|
+
line_number = None
|
|
52
|
+
error_line_content = None
|
|
53
|
+
if project_name:
|
|
54
|
+
for stack in reversed(formatted_traceback):
|
|
55
|
+
if project_name in stack.filename:
|
|
56
|
+
file_name = stack.filename
|
|
57
|
+
line_number = stack.lineno
|
|
58
|
+
error_line_content = stack.line
|
|
59
|
+
break
|
|
60
|
+
if not file_name and formatted_traceback:
|
|
61
|
+
stack = formatted_traceback[-1]
|
|
62
|
+
file_name = stack.filename
|
|
63
|
+
line_number = stack.lineno
|
|
64
|
+
error_line_content = stack.line
|
|
65
|
+
|
|
66
|
+
full_error_message = ''.join(
|
|
67
|
+
traceback.format_exception(exc_type, exc_value, exc_traceback)
|
|
68
|
+
)
|
|
69
|
+
file_content = ''
|
|
70
|
+
try:
|
|
71
|
+
with open(file_name, 'r', encoding='utf-8') as file:
|
|
72
|
+
file_content = file.read()
|
|
73
|
+
except (FileNotFoundError, PermissionError, UnicodeDecodeError, TypeError):
|
|
74
|
+
file_content = f"# Could not read file: {file_name}"
|
|
75
|
+
url_path = request.path
|
|
76
|
+
exception_type = str(type(exception).__name__)
|
|
77
|
+
http_method = request.method
|
|
78
|
+
status_code = 500
|
|
79
|
+
source = request.META.get('HTTP_X_CLIENT_TYPE', 'backend').lower()
|
|
80
|
+
if source not in ('backend', 'frontend'):
|
|
81
|
+
source = 'unknown'
|
|
82
|
+
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
83
|
+
if x_forwarded_for:
|
|
84
|
+
ip_address = x_forwarded_for.split(',')[0].strip()
|
|
85
|
+
else:
|
|
86
|
+
ip_address = request.META.get('REMOTE_ADDR')
|
|
87
|
+
user_agent = request.META.get('HTTP_USER_AGENT', '')[:1000]
|
|
88
|
+
|
|
89
|
+
request_data = None
|
|
90
|
+
try:
|
|
91
|
+
if hasattr(request, 'data'):
|
|
92
|
+
request_data = mask_sensitive_data(dict(request.data))
|
|
93
|
+
elif http_method in ('POST', 'PUT', 'PATCH'):
|
|
94
|
+
request_data = mask_sensitive_data(dict(request.POST))
|
|
95
|
+
except Exception:
|
|
96
|
+
request_data = None
|
|
97
|
+
with transaction.atomic():
|
|
98
|
+
existing_exception = ExceptionLog.objects.filter(
|
|
99
|
+
exception_type=exception_type,
|
|
100
|
+
file_name=file_name,
|
|
101
|
+
is_resolved=False,
|
|
102
|
+
).first()
|
|
103
|
+
|
|
104
|
+
if existing_exception:
|
|
105
|
+
existing_exception.count += 1
|
|
106
|
+
existing_exception.timestamp = timezone.now()
|
|
107
|
+
existing_exception.save(update_fields=['count', 'timestamp'])
|
|
108
|
+
else:
|
|
109
|
+
ExceptionLog.objects.create(
|
|
110
|
+
url_path=url_path,
|
|
111
|
+
exception_type=exception_type,
|
|
112
|
+
full_error_message=full_error_message,
|
|
113
|
+
file_name=file_name,
|
|
114
|
+
file_content=file_content,
|
|
115
|
+
line_number=line_number,
|
|
116
|
+
error_line_content=error_line_content,
|
|
117
|
+
user=user,
|
|
118
|
+
http_method=http_method,
|
|
119
|
+
status_code=status_code,
|
|
120
|
+
source=source,
|
|
121
|
+
ip_address=ip_address,
|
|
122
|
+
user_agent=user_agent,
|
|
123
|
+
request_data=request_data,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return None
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Generated by Django 6.1.1 on 2026-09-16 08:05
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name='AppSettings',
|
|
19
|
+
fields=[
|
|
20
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
21
|
+
('openai_api_key', models.CharField(blank=True, help_text="Required for AI recommendations. Get an OpenAI account; google 'openai api key'", max_length=255, null=True)),
|
|
22
|
+
('base_url', models.CharField(blank=True, help_text='ex: https://exceptbot.com', max_length=255, null=True)),
|
|
23
|
+
('project_name', models.CharField(blank=True, help_text="The directory name of your project. Ex: 'exceptbot'", max_length=255, null=True)),
|
|
24
|
+
],
|
|
25
|
+
options={
|
|
26
|
+
'verbose_name': 'App Setting',
|
|
27
|
+
'verbose_name_plural': 'App Settings',
|
|
28
|
+
},
|
|
29
|
+
),
|
|
30
|
+
migrations.CreateModel(
|
|
31
|
+
name='ExceptionLog',
|
|
32
|
+
fields=[
|
|
33
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
34
|
+
('exception_type', models.CharField(db_index=True, max_length=255)),
|
|
35
|
+
('full_error_message', models.TextField(blank=True)),
|
|
36
|
+
('file_name', models.CharField(db_index=True, max_length=500)),
|
|
37
|
+
('file_content', models.TextField(blank=True)),
|
|
38
|
+
('line_number', models.PositiveIntegerField(blank=True, null=True)),
|
|
39
|
+
('error_line_content', models.TextField(blank=True)),
|
|
40
|
+
('url_path', models.TextField(db_index=True)),
|
|
41
|
+
('http_method', models.CharField(db_index=True, default='GET', max_length=10)),
|
|
42
|
+
('status_code', models.PositiveIntegerField(blank=True, db_index=True, null=True)),
|
|
43
|
+
('source', models.CharField(choices=[('backend', 'Backend'), ('frontend', 'Frontend'), ('unknown', 'Unknown')], db_index=True, default='backend', max_length=20)),
|
|
44
|
+
('request_data', models.JSONField(blank=True, null=True)),
|
|
45
|
+
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
|
46
|
+
('user_agent', models.TextField(blank=True)),
|
|
47
|
+
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
48
|
+
('is_resolved', models.BooleanField(db_index=True, default=False)),
|
|
49
|
+
('resolved_at', models.DateTimeField(blank=True, null=True)),
|
|
50
|
+
('resolution_note', models.TextField(blank=True, default='')),
|
|
51
|
+
('ai_suggestion', models.TextField(blank=True, null=True)),
|
|
52
|
+
('count', models.PositiveIntegerField(default=1)),
|
|
53
|
+
('resolved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_exceptions', to=settings.AUTH_USER_MODEL)),
|
|
54
|
+
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='caused_exceptions', to=settings.AUTH_USER_MODEL)),
|
|
55
|
+
],
|
|
56
|
+
options={
|
|
57
|
+
'verbose_name': 'Exception Log',
|
|
58
|
+
'verbose_name_plural': 'Exception Logs',
|
|
59
|
+
'ordering': ['-timestamp'],
|
|
60
|
+
'indexes': [models.Index(fields=['is_resolved', '-timestamp'], name='exceptbot_e_is_reso_f0a0d5_idx'), models.Index(fields=['exception_type', 'file_name', 'is_resolved'], name='exceptbot_e_excepti_0cf5b5_idx'), models.Index(fields=['source', 'is_resolved'], name='exceptbot_e_source_e78949_idx')],
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
]
|
|
File without changes
|
exceptbot/models.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.utils import timezone
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExceptionLog(models.Model):
|
|
9
|
+
SOURCE_CHOICES = [
|
|
10
|
+
('backend', 'Backend'),
|
|
11
|
+
('frontend', 'Frontend'),
|
|
12
|
+
('unknown', 'Unknown'),
|
|
13
|
+
]
|
|
14
|
+
exception_type = models.CharField(max_length=255, db_index=True)
|
|
15
|
+
full_error_message = models.TextField(blank=True)
|
|
16
|
+
file_name = models.CharField(max_length=500, db_index=True)
|
|
17
|
+
file_content = models.TextField(blank=True)
|
|
18
|
+
line_number = models.PositiveIntegerField(null=True, blank=True)
|
|
19
|
+
error_line_content = models.TextField(blank=True)
|
|
20
|
+
url_path = models.TextField(db_index=True)
|
|
21
|
+
http_method = models.CharField(max_length=10, default='GET', db_index=True)
|
|
22
|
+
status_code = models.PositiveIntegerField(null=True, blank=True, db_index=True)
|
|
23
|
+
source = models.CharField(
|
|
24
|
+
max_length=20,
|
|
25
|
+
choices=SOURCE_CHOICES,
|
|
26
|
+
default='backend',
|
|
27
|
+
db_index=True,
|
|
28
|
+
)
|
|
29
|
+
request_data = models.JSONField(null=True, blank=True)
|
|
30
|
+
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
|
31
|
+
user_agent = models.TextField(blank=True)
|
|
32
|
+
user = models.ForeignKey(
|
|
33
|
+
settings.AUTH_USER_MODEL,
|
|
34
|
+
related_name='caused_exceptions',
|
|
35
|
+
null=True, blank=True,
|
|
36
|
+
on_delete=models.SET_NULL,
|
|
37
|
+
db_index=True,
|
|
38
|
+
)
|
|
39
|
+
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
40
|
+
is_resolved = models.BooleanField(default=False, db_index=True)
|
|
41
|
+
resolved_by = models.ForeignKey(
|
|
42
|
+
settings.AUTH_USER_MODEL,
|
|
43
|
+
related_name='resolved_exceptions',
|
|
44
|
+
null=True, blank=True,
|
|
45
|
+
on_delete=models.SET_NULL,
|
|
46
|
+
)
|
|
47
|
+
resolved_at = models.DateTimeField(null=True, blank=True)
|
|
48
|
+
resolution_note = models.TextField(blank=True, default='')
|
|
49
|
+
ai_suggestion = models.TextField(null=True, blank=True)
|
|
50
|
+
count = models.PositiveIntegerField(default=1)
|
|
51
|
+
|
|
52
|
+
class Meta:
|
|
53
|
+
ordering = ['-timestamp']
|
|
54
|
+
verbose_name = 'Exception Log'
|
|
55
|
+
verbose_name_plural = 'Exception Logs'
|
|
56
|
+
indexes = [
|
|
57
|
+
models.Index(fields=['is_resolved', '-timestamp']),
|
|
58
|
+
models.Index(fields=['exception_type', 'file_name', 'is_resolved']),
|
|
59
|
+
models.Index(fields=['source', 'is_resolved']),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
def __str__(self):
|
|
63
|
+
return f"[{self.source}] {self.exception_type} at {self.url_path}"
|
|
64
|
+
|
|
65
|
+
def mark_resolved(self, user, note=''):
|
|
66
|
+
self.is_resolved = True
|
|
67
|
+
self.resolved_by = user
|
|
68
|
+
self.resolved_at = timezone.now()
|
|
69
|
+
self.resolution_note = note
|
|
70
|
+
self.save()
|
|
71
|
+
|
|
72
|
+
def get_blocks(self):
|
|
73
|
+
if not self.ai_suggestion:
|
|
74
|
+
return []
|
|
75
|
+
blocks = re.split(r'(```[a-zA-Z]*\n)', self.ai_suggestion)
|
|
76
|
+
formatted_blocks = []
|
|
77
|
+
in_code_block = False
|
|
78
|
+
language = None
|
|
79
|
+
for block in blocks:
|
|
80
|
+
if block.startswith('```'):
|
|
81
|
+
in_code_block = not in_code_block
|
|
82
|
+
language = block[3:].strip()
|
|
83
|
+
if language == '':
|
|
84
|
+
language = None
|
|
85
|
+
elif in_code_block:
|
|
86
|
+
formatted_blocks.append({
|
|
87
|
+
'text': block.strip(),
|
|
88
|
+
'is_code': True,
|
|
89
|
+
'language': language,
|
|
90
|
+
})
|
|
91
|
+
else:
|
|
92
|
+
formatted_blocks.append({
|
|
93
|
+
'text': block,
|
|
94
|
+
'is_code': False,
|
|
95
|
+
'language': None,
|
|
96
|
+
})
|
|
97
|
+
return formatted_blocks
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AppSettings(models.Model):
|
|
101
|
+
openai_api_key = models.CharField(
|
|
102
|
+
max_length=255,
|
|
103
|
+
blank=True,
|
|
104
|
+
null=True,
|
|
105
|
+
help_text="Required for AI recommendations. Get an OpenAI account; google 'openai api key'",
|
|
106
|
+
)
|
|
107
|
+
base_url = models.CharField(
|
|
108
|
+
max_length=255,
|
|
109
|
+
blank=True,
|
|
110
|
+
null=True,
|
|
111
|
+
help_text="ex: https://exceptbot.com",
|
|
112
|
+
)
|
|
113
|
+
project_name = models.CharField(
|
|
114
|
+
max_length=255,
|
|
115
|
+
blank=True,
|
|
116
|
+
null=True,
|
|
117
|
+
help_text="The directory name of your project. Ex: 'exceptbot'",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
class Meta:
|
|
121
|
+
verbose_name = 'App Setting'
|
|
122
|
+
verbose_name_plural = 'App Settings'
|
|
123
|
+
|
|
124
|
+
def __str__(self):
|
|
125
|
+
return self.project_name or 'App Settings'
|
|
126
|
+
|
|
127
|
+
def save(self, *args, **kwargs):
|
|
128
|
+
self.pk = 1
|
|
129
|
+
super().save(*args, **kwargs)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def get_solo(cls):
|
|
133
|
+
obj, _ = cls.objects.get_or_create(pk=1)
|
|
134
|
+
return obj
|
exceptbot/permissions.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from rest_framework.permissions import BasePermission
|
|
2
|
+
|
|
3
|
+
class IsSuperUser(BasePermission):
|
|
4
|
+
message = 'Only superusers can access ExceptBot endpoints.'
|
|
5
|
+
|
|
6
|
+
def has_permission(self, request, view):
|
|
7
|
+
return bool(
|
|
8
|
+
request.user
|
|
9
|
+
and request.user.is_authenticated
|
|
10
|
+
and request.user.is_superuser
|
|
11
|
+
)
|
exceptbot/serializers.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
from .models import ExceptionLog, AppSettings
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ExceptionLogListSerializer(serializers.ModelSerializer):
|
|
6
|
+
user = serializers.StringRelatedField(read_only=True)
|
|
7
|
+
resolved_by = serializers.StringRelatedField(read_only=True)
|
|
8
|
+
source_display = serializers.SerializerMethodField()
|
|
9
|
+
|
|
10
|
+
class Meta:
|
|
11
|
+
model = ExceptionLog
|
|
12
|
+
fields = [
|
|
13
|
+
'id',
|
|
14
|
+
'exception_type',
|
|
15
|
+
'url_path',
|
|
16
|
+
'http_method',
|
|
17
|
+
'status_code',
|
|
18
|
+
'source',
|
|
19
|
+
'source_display',
|
|
20
|
+
'file_name',
|
|
21
|
+
'line_number',
|
|
22
|
+
'user',
|
|
23
|
+
'count',
|
|
24
|
+
'is_resolved',
|
|
25
|
+
'resolved_by',
|
|
26
|
+
'resolved_at',
|
|
27
|
+
'timestamp',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def get_source_display(self, obj):
|
|
31
|
+
return obj.get_source_display()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ExceptionLogDetailSerializer(serializers.ModelSerializer):
|
|
36
|
+
user = serializers.StringRelatedField(read_only=True)
|
|
37
|
+
resolved_by = serializers.StringRelatedField(read_only=True)
|
|
38
|
+
source_display = serializers.SerializerMethodField()
|
|
39
|
+
|
|
40
|
+
class Meta:
|
|
41
|
+
model = ExceptionLog
|
|
42
|
+
fields = '__all__'
|
|
43
|
+
|
|
44
|
+
def get_source_display(self, obj):
|
|
45
|
+
return obj.get_source_display()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ExceptionLogResolveSerializer(serializers.Serializer):
|
|
50
|
+
resolution_note = serializers.CharField(
|
|
51
|
+
required=False,
|
|
52
|
+
allow_blank=True,
|
|
53
|
+
default='',
|
|
54
|
+
max_length=2000,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AppSettingsSerializer(serializers.ModelSerializer):
|
|
60
|
+
class Meta:
|
|
61
|
+
model = AppSettings
|
|
62
|
+
fields = ['openai_api_key', 'base_url', 'project_name']
|
|
63
|
+
extra_kwargs = {
|
|
64
|
+
'openai_api_key': {'write_only': False, 'required': False},
|
|
65
|
+
}
|
exceptbot/tests.py
ADDED
|
File without changes
|