Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion lib/GADS/API.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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;
}

Expand Down
21 changes: 20 additions & 1 deletion lib/GADS/Audit.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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) = @_;
Expand Down
62 changes: 45 additions & 17 deletions src/frontend/js/lib/logging.js
Original file line number Diff line number Diff line change
@@ -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;
}
}

Expand Down
34 changes: 34 additions & 0 deletions src/frontend/js/lib/util/scriptErrorHandler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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) {
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 };
Original file line number Diff line number Diff line change
@@ -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/'
});
});
});
33 changes: 33 additions & 0 deletions src/frontend/js/lib/util/scriptErrorHandler/lib/MessageUploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 uploader = new Uploader('/api/script_error', '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<void> {
method ||= 'N/A';
const csrf_token = document.body.dataset.csrf;
const body = {
description,
method,
url: window.location.href,
csrf_token
};
try {
return await this.uploader.upload(body);
} catch (err) {
console.error("Failed to upload message:", err);
}
}
}
3 changes: 2 additions & 1 deletion src/frontend/js/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -87,4 +88,4 @@ registerComponent(AutosaveComponent);
// Initialize all components at some point
initializeRegisteredComponents(document.body);

handleActions();
handleActions();