diff --git a/lib/GADS/API.pm b/lib/GADS/API.pm index fc1be173f..43a484215 100644 --- a/lib/GADS/API.pm +++ b/lib/GADS/API.pm @@ -1464,6 +1464,15 @@ get '/api/get_key' => require_login sub { } }; +post '/api/script_error' => require_login sub { + my $body = _decode_json_body(); + + info __x "SCRIPT ERROR: {url} - {description}", + url => $body->{url}, description => $body->{description}; + + _success("Script error logged successfully"); +}; + sub _success { my $msg = shift; send_as JSON => { 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..47cd1b857 --- /dev/null +++ b/src/frontend/js/lib/util/scriptErrorHandler/index.ts @@ -0,0 +1,31 @@ +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) { + if (location.host === 'localhost') { + // If we're on localhost, we log the error to the console. This is useful for development. + console.error("Script error occurred:", message, source, lineno, colno, error); + } + 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) + + uploadMessage(description) + .catch(err => { + console.error("Failed to upload script error:", err); + }); +} + +export { uploadMessage }; 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..b7725c100 --- /dev/null +++ b/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.test.ts @@ -0,0 +1,23 @@ +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), + url: 'http://localhost/' + }); + }); +}); 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..b362cfeb7 --- /dev/null +++ b/src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.ts @@ -0,0 +1,30 @@ +import { Uploader } from "util/upload/UploadControl"; + +export const uploadMessage = async (message: string) => { + const body = { + description: message, + url: window.location.href + }; + const uploader = new Uploader('/api/script_error', 'POST'); + const messageUploader = new MessageUploader(uploader); + return await messageUploader.uploadMessage(body.description); +}; + +export class MessageUploader { + constructor(private uploader: Uploader) { + } + + async uploadMessage(description: string): Promise { + const csrf_token = document.body.dataset.csrf; + const body = { + description, + url: window.location.href, + csrf_token + }; + try { + return await this.uploader.upload(body); + } catch (err) { + console.error("Failed to upload message:", err); + } + } +} 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();