Upgrade Sentry to v8 and remove custom Dependabot grouping

As part of the upgrade process, we need to rework the tracing instrumentation. Now, we are just wrapping all async functions in a new sentry transaction, which will automatically end once the function returns.

Further, the structure of the Sentry packages got reworked, so that we only need a single package by now. This removes the need to group dependabot updates.

Co-authored-by: Jan Graichen <jgraichen@altimos.de>
This commit is contained in:
Sebastian Serth
2024-05-20 14:00:55 +02:00
committed by Sebastian Serth
parent 86c67f3c9a
commit 94404370c4
12 changed files with 234 additions and 300 deletions

View File

@ -202,15 +202,25 @@ var CodeOceanEditor = {
$('button i.fa-spin').removeClass('d-inline-block').addClass('d-none');
},
startSentryTransaction: function (initiator) {
const cause = initiator.data('cause') || initiator.prop('id');
this.sentryTransaction = window.SentryUtils.startIdleTransaction(
Sentry.getCurrentHub(),
{ name: cause, op: "transaction" },
0, // Idle Timeout
window.SentryUtils.TRACING_DEFAULTS.finalTimeout,
true); // onContext
Sentry.getCurrentHub().configureScope(scope => scope.setSpan(this.sentryTransaction));
newSentryTransaction: function (initiator, callback) {
// based on Sentry recommendation.
// See https://github.com/getsentry/sentry-javascript/issues/12116
return Sentry.continueTrace({ sentryTrace: '', baggage: '' }, () => {
// inside of this we have a new trace!
return Sentry.withActiveSpan(null, () => {
// inside of this there is no parent span, no matter what!
const cause = initiator.data('cause') || initiator.prop('id');
return Sentry.startSpan({name: cause, op: "transaction", forceTransaction: true}, async () => {
// Execute the desired custom code
try {
return await callback();
} catch (error) {
console.error(error);
Sentry.captureException(error, {mechanism: {handled: false}});
}
});
});
});
},
resizeAceEditors: function (own_solution = false) {

View File

@ -2,7 +2,6 @@ CodeOceanEditorEvaluation = {
// A list of non-printable characters that are not allowed in the code output.
// Taken from https://stackoverflow.com/a/69024306
nonPrintableRegEx: /[\u0000-\u0008\u000B\u000C\u000F-\u001F\u007F-\u009F\u2000-\u200F\u2028-\u202F\u205F-\u206F\u3000\uFEFF]/g,
sentryTransaction: null,
/**
* Scoring-Functions
@ -10,17 +9,18 @@ CodeOceanEditorEvaluation = {
scoreCode: function (event) {
event.preventDefault();
const cause = $('#assess');
this.startSentryTransaction(cause);
this.stopCode(event);
this.clearScoringOutput();
$('#submit').addClass("d-none");
this.newSentryTransaction(cause, async () => {
this.stopCode(event);
this.clearScoringOutput();
$('#submit').addClass("d-none");
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
this.showSpinner($('#assess'));
$('#score_div').removeClass('d-none');
await this.socketScoreCode(submission.id);
this.showSpinner($('#assess'));
$('#score_div').removeClass('d-none');
await this.socketScoreCode(submission.id);
});
},
handleScoringResponse: function (results) {

View File

@ -8,7 +8,7 @@ CodeOceanEditorWebsocket = {
params.protocol = this.webSocketProtocol;
params._options = true;
// 2. Create a new Sentry transaction.
// 2. Create a new Sentry span.
// Since we want to group similar URLs, we use the URL without the ID and filename as the description.
const cleanedUrl = urlHelper({
...params,
@ -16,54 +16,54 @@ CodeOceanEditorWebsocket = {
...(params.filename && {filename: '*'}), // Overwrite the filename with a wildcard only if it is present.
});
const sentryDescription = `WebSocket ${cleanedUrl}`;
const span = this.sentryTransaction?.startChild({op: 'websocket.client', description: sentryDescription, data: {...params}})
return Sentry.startSpan({op: 'websocket.client', name: sentryDescription, attributes: {...params}}, async webSocketSpan => {
// 3. Create the actual WebSocket URL.
// This URL might contain Sentry Tracing headers to propagate the Sentry transaction.
if (span) {
const dynamicContext = this.sentryTransaction.getDynamicSamplingContext();
const baggage = SentryUtils.dynamicSamplingContextToSentryBaggageHeader(dynamicContext);
if (baggage) {
params.HTTP_SENTRY_TRACE = span.toTraceparent();
params.HTTP_BAGGAGE = baggage;
// 3. Create the actual WebSocket URL.
// This URL might contain Sentry Tracing headers to propagate the Sentry transaction.
if (webSocketSpan) {
params.HTTP_SENTRY_TRACE = Sentry.spanToTraceHeader(webSocketSpan);
const baggage = Sentry.spanToBaggageHeader(webSocketSpan);
if (baggage) {
params.HTTP_BAGGAGE = Sentry.spanToBaggageHeader(webSocketSpan);
}
}
}
const url = urlHelper({...params});
const url = urlHelper({...params});
// 4. Connect to the given URL.
this.websocket = new CommandSocket(url,
function (evt) {
this.resetOutputTab();
}.bind(this)
);
// 4. Connect to the given URL.
this.websocket = new CommandSocket(url,
function (evt) {
this.resetOutputTab();
}.bind(this)
);
// Attach custom handlers for messages received.
setupFunction(this.websocket);
// Attach custom handlers for messages received.
setupFunction(this.websocket);
CodeOceanEditorWebsocket.websocket = this.websocket;
CodeOceanEditorWebsocket.websocket = this.websocket;
// Create and return a new Promise. It will only resolve (or fail) once the connection has ended.
return new Promise((resolve, reject) => {
this.websocket.onError(this.showWebsocketError.bind(this));
// Create and return a new Promise. It will only resolve (or fail) once the connection has ended.
return new Promise((resolve, reject) => {
this.websocket.onError(this.showWebsocketError.bind(this));
// Remove event listeners for Promise handling.
// This is especially useful in case of an error, where a `close` event might follow the `error` event.
const teardown = () => {
this.websocket.websocket.removeEventListener(closeListener);
this.websocket.websocket.removeEventListener(errorListener);
};
// Remove event listeners for Promise handling.
// This is especially useful in case of an error, where a `close` event might follow the `error` event.
const teardown = () => {
this.websocket.websocket.removeEventListener(closeListener);
this.websocket.websocket.removeEventListener(errorListener);
};
// We are using event listeners (and not `onError` or `onClose`) here, since these listeners should never be overwritten.
// With `onError` or `onClose`, a new assignment would overwrite a previous one.
const closeListener = this.websocket.websocket.addEventListener('close', () => {
span?.finish();
resolve();
teardown();
});
const errorListener = this.websocket.websocket.addEventListener('error', (error) => {
reject(error);
teardown();
this.websocket.killWebSocket(); // In case of error, ensure we always close the connection.
// We are using event listeners (and not `onError` or `onClose`) here, since these listeners should never be overwritten.
// With `onError` or `onClose`, a new assignment would overwrite a previous one.
const closeListener = this.websocket.websocket.addEventListener('close', () => {
resolve();
teardown();
});
const errorListener = this.websocket.websocket.addEventListener('error', (error) => {
reject(error);
teardown();
this.websocket.killWebSocket(); // In case of error, ensure we always close the connection.
});
});
});
},

View File

@ -110,46 +110,47 @@ CodeOceanEditorRequestForComments = {
const cause = $('#requestComments');
const editor = $('#editor')
const questionElement = $('#question')
this.startSentryTransaction(cause);
questionElement.prop("disabled", true);
$('#closeAskForCommentsButton').addClass('d-none');
this.newSentryTransaction(cause, async () => {
questionElement.prop("disabled", true);
$('#closeAskForCommentsButton').addClass('d-none');
const exercise_id = editor.data('exercise-id');
const file_id = $('.editor').data('id');
const question = questionElement.val();
const exercise_id = editor.data('exercise-id');
const file_id = $('.editor').data('id');
const question = questionElement.val();
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
this.showSpinner($('#askForCommentsButton'));
this.showSpinner($('#askForCommentsButton'));
const response = await $.ajax({
method: 'POST',
url: Routes.request_for_comments_path(),
data: {
request_for_comment: {
exercise_id: exercise_id,
file_id: file_id,
submission_id: submission.id,
question: question
const response = await $.ajax({
method: 'POST',
url: Routes.request_for_comments_path(),
data: {
request_for_comment: {
exercise_id: exercise_id,
file_id: file_id,
submission_id: submission.id,
question: question
}
}
}).catch(this.ajaxError.bind(this));
bootstrap.Modal.getInstance($('#comment-modal')).hide();
this.hideSpinner();
$('#question').prop("disabled", false).val('');
$('#closeAskForCommentsButton').removeClass('d-none');
$('#askForCommentsButton').one('click', this.requestComments.bind(this));
// we disabled the button to prevent that the user spams RFCs, but decided against this now.
//var button = $('#requestComments');
//button.prop('disabled', true);
if (response) {
await this.runSubmission(submission);
$.flash.success({text: $('#askForCommentsButton').data('message-success')});
}
}).catch(this.ajaxError.bind(this));
bootstrap.Modal.getInstance($('#comment-modal')).hide();
this.hideSpinner();
$('#question').prop("disabled", false).val('');
$('#closeAskForCommentsButton').removeClass('d-none');
$('#askForCommentsButton').one('click', this.requestComments.bind(this));
// we disabled the button to prevent that the user spams RFCs, but decided against this now.
//var button = $('#requestComments');
//button.prop('disabled', true);
if (response) {
await this.runSubmission(submission);
$.flash.success({text: $('#askForCommentsButton').data('message-success')});
}
});
}
};

View File

@ -108,19 +108,20 @@ CodeOceanEditorSubmissions = {
},
resetCode: function(initiator, onlyActiveFile = false) {
this.startSentryTransaction(initiator);
this.showSpinner(initiator);
this.newSentryTransaction(initiator, async () => {
this.showSpinner(initiator);
const response = await this.ajax({
method: 'GET',
url: $('#start-over').data('url') || $('#start-over-active-file').data('url')
}).catch(this.ajaxError.bind(this));
const response = await this.ajax({
method: 'GET',
url: $('#start-over').data('url') || $('#start-over-active-file').data('url')
}).catch(this.ajaxError.bind(this));
this.hideSpinner();
this.hideSpinner();
if (!response) return;
App.synchronized_editor?.reset_content(response);
this.setEditorContent(response, onlyActiveFile);
if (!response) return;
App.synchronized_editor?.reset_content(response);
this.setEditorContent(response, onlyActiveFile);
});
},
setEditorContent: function(new_content, onlyActiveFile = false) {
@ -140,32 +141,33 @@ CodeOceanEditorSubmissions = {
renderCode: function(event) {
event.preventDefault();
const cause = $('#render');
this.startSentryTransaction(cause);
if (!cause.is(':visible')) return;
this.newSentryTransaction(cause, async () => {
if (!cause.is(':visible')) return;
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
if (submission.render_url === undefined) return;
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
if (submission.render_url === undefined) return;
const active_file = CodeOceanEditor.active_file.filename;
const desired_file = submission.render_url.filter(hash => hash.filepath === active_file);
const url = desired_file[0].url;
const active_file = CodeOceanEditor.active_file.filename;
const desired_file = submission.render_url.filter(hash => hash.filepath === active_file);
const url = desired_file[0].url;
// Allow to open the new tab even in Safari.
// See: https://stackoverflow.com/a/70463940
setTimeout(() => {
var pop_up_window = window.open(url, '_blank');
if (pop_up_window) {
pop_up_window.onerror = function (message) {
this.clearOutput();
this.printOutput({
stderr: message
}, true, 0);
this.sendError(message, submission.id);
this.showOutputBar();
};
}
})
// Allow to open the new tab even in Safari.
// See: https://stackoverflow.com/a/70463940
setTimeout(() => {
var pop_up_window = window.open(url, '_blank');
if (pop_up_window) {
pop_up_window.onerror = function (message) {
this.clearOutput();
this.printOutput({
stderr: message
}, true, 0);
this.sendError(message, submission.id);
this.showOutputBar();
};
}
})
});
},
/**
@ -174,14 +176,15 @@ CodeOceanEditorSubmissions = {
runCode: function(event) {
event.preventDefault();
const cause = $('#run');
this.startSentryTransaction(cause);
this.stopCode(event);
if (!cause.is(':visible')) return;
this.newSentryTransaction(cause, async () => {
this.stopCode(event);
if (!cause.is(':visible')) return;
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
await this.runSubmission(submission);
await this.runSubmission(submission);
});
},
runSubmission: async function (submission) {
@ -196,15 +199,16 @@ CodeOceanEditorSubmissions = {
testCode: function(event) {
event.preventDefault();
const cause = $('#test');
this.startSentryTransaction(cause);
if (!cause.is(':visible')) return;
this.newSentryTransaction(cause, async () => {
if (!cause.is(':visible')) return;
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
this.showSpinner($('#test'));
$('#score_div').addClass('d-none');
await this.socketTestCode(submission.id, CodeOceanEditor.active_file.filename);
this.showSpinner($('#test'));
$('#score_div').addClass('d-none');
await this.socketTestCode(submission.id, CodeOceanEditor.active_file.filename);
});
},
/**