From 2dec15b59bb17bd172c359be0392b593226566af Mon Sep 17 00:00:00 2001 From: Dave Roberts Date: Fri, 21 Nov 2025 13:04:34 +0000 Subject: [PATCH 1/2] Completed error logging to Audit Also add logging upload to logging JS --- lib/GADS/API.pm | 17 ++++- lib/GADS/Audit.pm | 21 ++++++- src/frontend/js/lib/logging.js | 62 ++++++++++++++----- .../js/lib/util/scriptErrorHandler/index.ts | 35 +++++++++++ .../lib/MessageUploader.test.ts | 24 +++++++ .../scriptErrorHandler/lib/MessageUploader.ts | 32 ++++++++++ src/frontend/js/site.js | 3 +- 7 files changed, 174 insertions(+), 20 deletions(-) create mode 100644 src/frontend/js/lib/util/scriptErrorHandler/index.ts create mode 100644 src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.test.ts create mode 100644 src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.ts diff --git a/lib/GADS/API.pm b/lib/GADS/API.pm index c0a92b940..d05fd5a44 100644 --- a/lib/GADS/API.pm +++ b/lib/GADS/API.pm @@ -1461,6 +1461,21 @@ get '/api/get_key' => require_login sub { } }; +post '/api/script_error' => require_login sub { + my $user = logged_in_user; + my $body = _decode_json_body(); + + my $logger = GADS::Audit->new( + user => $user, + schema => schema, + ); + + $logger->script_error(%$body); + + content_type 'application/json; charset=UTF-8'; + return success "Script error logged successfully"; +}; + sub _success { my $msg = shift; send_as JSON => { @@ -1476,7 +1491,7 @@ sub _decode_json_body if request->content_type ne 'application/json'; my $body = try { decode_json request->body } - or error __"Failed to decode JSON"; + or error __"Failed to decode JSON" . " - " . $@; $body; } diff --git a/lib/GADS/Audit.pm b/lib/GADS/Audit.pm index f2da65450..946913503 100644 --- a/lib/GADS/Audit.pm +++ b/lib/GADS/Audit.pm @@ -70,7 +70,26 @@ has filtering => ( builder => sub { +{} }, ); -sub audit_types{ [qw/user_action login_change login_success logout login_failure/] }; +sub audit_types{ [qw/user_action login_change login_success logout login_failure script_error/] }; + +sub script_error +{ + my ($self, %options) = @_; + + my $description = $options{description} || 'Script error'; + my $method = $options{method} || 'unknown'; + my $url = $options{url} || 'unknown'; + + $self->schema->resultset('Audit')->create({ + user_id => $self->user_id, + description => $description, + type => 'script_error', + method => $method, + url => $url, + datetime => DateTime->now, + instance_id => $options{instance_id}, + }); +} sub user_action { my ($self, %options) = @_; diff --git a/src/frontend/js/lib/logging.js b/src/frontend/js/lib/logging.js index bea4144c4..4799b610a 100644 --- a/src/frontend/js/lib/logging.js +++ b/src/frontend/js/lib/logging.js @@ -1,34 +1,62 @@ +import { uploadMessage } from "util/scriptErrorHandler"; + class Logging { - constructor() { - this.allowLogging = - window.test || - location.hostname === 'localhost' || - location.hostname === '127.0.0.1' || - location.hostname.endsWith('.peek.digitpaint.nl') + constructor() { + this.allowLogging = + window.test || + location.hostname === 'localhost' || + location.hostname === '127.0.0.1' || + location.hostname.endsWith('.peek.digitpaint.nl') } log(...message) { - if(this.allowLogging) { - console.log(message) - } + if (this.allowLogging) { + console.log(message) + } else { + const message = this.formatMessage('log', ...message) + uploadMessage(message) + } } info(...message) { - if(this.allowLogging) { - console.info(message) - } + if (this.allowLogging) { + console.info(message) + } else { + const message = this.formatMessage('info', ...message) + uploadMessage(message) + } } warn(...message) { - if(this.allowLogging) { - console.warn(message) - } + if (this.allowLogging) { + console.warn(message) + } else { + const message = this.formatMessage('warn', ...message) + uploadMessage(message) + } } error(...message) { - if(this.allowLogging) { - console.error(message) + if (this.allowLogging) { + console.error(message) + } else { + const message = this.formatMessage('error', ...message) + uploadMessage(message) + } + } + + formatMessage(type, ...message) { + let output = type + ': '; + for (let i = 0; i < message.length; i++) { + if (typeof message[i] === 'object') { + output += JSON.stringify(message[i]); + } else { + // This is wrapped so that anything that's not an object is converted to a string + output += `${message[i]}`; } + if (i < message.length - 1) output += ' '; + } + return output; } } diff --git a/src/frontend/js/lib/util/scriptErrorHandler/index.ts b/src/frontend/js/lib/util/scriptErrorHandler/index.ts new file mode 100644 index 000000000..68411f0b0 --- /dev/null +++ b/src/frontend/js/lib/util/scriptErrorHandler/index.ts @@ -0,0 +1,35 @@ +import { logging } from "logging"; +import { uploadMessage } from "./lib/MessageUploader"; + +const createErrorString = (message: string, source: any, lineno: number, colno: number, error: Error | string | null) => { + let errorString = `Error: ${message}\nSource: ${source}\nLine: ${lineno}, Column: ${colno}`; + if (error && (error as Error)?.stack) { + errorString += `\nStack: ${(error as Error)?.stack}`; + } + return errorString; +} + +window.onerror = function (message: string, source: any, lineno: number, colno: number, error: Error | string | null) { + console.log("location.pathname", location.pathname); + if (location.host === 'localhost') { + // If we're on localhost, we log the error to the console. This is useful for development. + logging.error("Script error occurred:", message, source, lineno, colno, error); + return; + } + if (location.pathname === '/api/script_error' || location.pathname === '/login') { + // If we're on the script error page, we don't want to log it again. + console.error("Script error occurred but not logged to avoid recursion."); + console.error(createErrorString(message, source, lineno, colno, error)); + return; + } + const description = createErrorString(message, source, lineno, colno, error) + console.log("Script error occurred:", description); + const method = 'N/A'; + + uploadMessage(description, method) + .catch(err => { + console.error("Failed to upload script error:", err); + }); +} + +export { uploadMessage }; \ No newline at end of file diff --git a/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.test.ts b/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.test.ts new file mode 100644 index 000000000..ad82189bd --- /dev/null +++ b/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect, jest } from '@jest/globals'; +import { MessageUploader } from './MessageUploader'; + +describe('MessageUploader', () => { + class MockUploader { + upload = jest.fn(); + } + + it('should upload messages correctly', async () => { + const uploader = new MockUploader(); + const messageUploader = new MessageUploader(uploader); + + const messages = { id: 1, content: 'Test message 1' }; + + await messageUploader.uploadMessage(JSON.stringify(messages)); + + expect(uploader.upload).toHaveBeenCalledTimes(1); + expect(uploader.upload).toHaveBeenCalledWith({ + description: JSON.stringify(messages), + method: 'N/A', + url: 'http://localhost/' + }); + }); +}); \ No newline at end of file diff --git a/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.ts b/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.ts new file mode 100644 index 000000000..e9c3023ba --- /dev/null +++ b/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.ts @@ -0,0 +1,32 @@ +import { Uploader } from "util/upload/UploadControl"; + +export const uploadMessage = async (message: string, method?: string) => { + const body = { + description: message, + method: method || 'N/A', + url: window.location.href + }; + const token = document.body.dataset.csrf; + const uploader = new Uploader('/api/script_error?csrf_token='+token, 'POST'); + const messageUploader = new MessageUploader(uploader); + return await messageUploader.uploadMessage(body.description, body.method); +}; + +export class MessageUploader { + constructor(private uploader: Uploader) { + } + + async uploadMessage(description: string, method?: string): Promise { + method ||= 'N/A'; + const body = { + description, + method, + url: window.location.href + }; + try { + return await this.uploader.upload(body); + } catch (err) { + console.error("Failed to upload message:", err); + } + } +} \ No newline at end of file diff --git a/src/frontend/js/site.js b/src/frontend/js/site.js index 1517accdf..e93f46bbc 100644 --- a/src/frontend/js/site.js +++ b/src/frontend/js/site.js @@ -4,6 +4,7 @@ import 'bootstrap'; import 'components/graph/lib/chart'; import 'util/filedrag'; import 'util/actionsHandler'; +import 'util/scriptErrorHandler'; // Components import AddTableModalComponent from 'components/modal/modals/new-table'; @@ -87,4 +88,4 @@ registerComponent(AutosaveComponent); // Initialize all components at some point initializeRegisteredComponents(document.body); -handleActions(); \ No newline at end of file +handleActions(); From aab5e8dd2b28a3e1b8a4388c9994330fee328462 Mon Sep 17 00:00:00 2001 From: Dave Roberts Date: Tue, 23 Dec 2025 15:23:23 +0000 Subject: [PATCH 2/2] Fix for CRSF token error --- src/frontend/js/lib/util/scriptErrorHandler/index.ts | 1 - .../js/lib/util/scriptErrorHandler/lib/MessageUploader.ts | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frontend/js/lib/util/scriptErrorHandler/index.ts b/src/frontend/js/lib/util/scriptErrorHandler/index.ts index 68411f0b0..4a8b389db 100644 --- a/src/frontend/js/lib/util/scriptErrorHandler/index.ts +++ b/src/frontend/js/lib/util/scriptErrorHandler/index.ts @@ -10,7 +10,6 @@ const createErrorString = (message: string, source: any, lineno: number, colno: } window.onerror = function (message: string, source: any, lineno: number, colno: number, error: Error | string | null) { - console.log("location.pathname", location.pathname); if (location.host === 'localhost') { // If we're on localhost, we log the error to the console. This is useful for development. logging.error("Script error occurred:", message, source, lineno, colno, error); diff --git a/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.ts b/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.ts index e9c3023ba..bc3a88952 100644 --- a/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.ts +++ b/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.ts @@ -6,8 +6,7 @@ export const uploadMessage = async (message: string, method?: string) => { method: method || 'N/A', url: window.location.href }; - const token = document.body.dataset.csrf; - const uploader = new Uploader('/api/script_error?csrf_token='+token, 'POST'); + const uploader = new Uploader('/api/script_error', 'POST'); const messageUploader = new MessageUploader(uploader); return await messageUploader.uploadMessage(body.description, body.method); }; @@ -18,10 +17,12 @@ export class MessageUploader { async uploadMessage(description: string, method?: string): Promise { method ||= 'N/A'; + const csrf_token = document.body.dataset.csrf; const body = { description, method, - url: window.location.href + url: window.location.href, + csrf_token }; try { return await this.uploader.upload(body);