From 240fbc5a3b99a51c2938948995e9279831b0f8d3 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Tue, 9 May 2023 21:15:23 +0200 Subject: [PATCH] Add Sentry instrumentation for JavaScript --- app/assets/javascripts/base.js | 22 ++-- app/assets/javascripts/editor/editor.js.erb | 10 ++ app/assets/javascripts/editor/evaluation.js | 5 +- app/assets/javascripts/editor/execution.js | 14 ++- app/assets/javascripts/editor/submissions.js | 14 ++- app/assets/javascripts/editor/websocket.js | 4 + app/javascript/application.js | 11 ++ config/application.rb | 4 + config/initializers/inflections.rb | 1 + lib/middleware/websocket_sentry_headers.rb | 28 +++++ package.json | 5 +- yarn.lock | 107 ++++++++++++------- 12 files changed, 166 insertions(+), 59 deletions(-) create mode 100644 lib/middleware/websocket_sentry_headers.rb diff --git a/app/assets/javascripts/base.js b/app/assets/javascripts/base.js index e28347a0..d181d4c6 100644 --- a/app/assets/javascripts/base.js +++ b/app/assets/javascripts/base.js @@ -45,27 +45,23 @@ $(document).on('turbolinks:load', function() { // Initialize Sentry const sentrySettings = $('meta[name="sentry"]') if (sentrySettings.data()['enabled']) { - // Workaround for Turbolinks: We must not re-initialize the Relay object when visiting another page - window.SentryReplay ||= new Sentry.Replay(); - Sentry.init({ dsn: sentrySettings.data('dsn'), attachStacktrace: true, release: sentrySettings.data('release'), environment: sentrySettings.data('environment'), - autoSessionTracking: false, + autoSessionTracking: true, + tracesSampleRate: 1.0, replaysSessionSampleRate: 0.0, replaysOnErrorSampleRate: 1.0, - integrations: [ - SentryReplay, - ], - }); + integrations: window.SentryIntegrations, + initialScope: scope =>{ + const user = $('meta[name="current-user"]').attr('content'); - Sentry.configureScope(function (scope) { - const user = $('meta[name="current-user"]').attr('content'); - - if (user) { - scope.setUser(JSON.parse(user)); + if (user) { + scope.setUser(JSON.parse(user)); + } + return scope; } }); } diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 9d70d58e..6246ecdc 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -212,6 +212,16 @@ 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)); + }, resizeAceEditors: function (own_solution = false) { let editorSelector; diff --git a/app/assets/javascripts/editor/evaluation.js b/app/assets/javascripts/editor/evaluation.js index 94894edb..e12e41a0 100644 --- a/app/assets/javascripts/editor/evaluation.js +++ b/app/assets/javascripts/editor/evaluation.js @@ -3,16 +3,19 @@ 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 */ scoreCode: function (event) { + const cause = $('#assess'); + this.startSentryTransaction(cause); event.preventDefault(); this.stopCode(event); this.clearScoringOutput(); $('#submit').addClass("d-none"); - this.createSubmission('#assess', null, function (response) { + this.createSubmission(cause, null, function (response) { this.showSpinner($('#assess')); $('#score_div').removeClass('d-none'); var url = response.score_url; diff --git a/app/assets/javascripts/editor/execution.js b/app/assets/javascripts/editor/execution.js index d9787545..61f1f34c 100644 --- a/app/assets/javascripts/editor/execution.js +++ b/app/assets/javascripts/editor/execution.js @@ -1,7 +1,7 @@ CodeOceanEditorWebsocket = { websocket: null, - createSocketUrl: function(url) { + createSocketUrl: function(url, span) { const sockURL = new URL(url, window.location); // not needed any longer, we put it directly into the url: sockURL.pathname = url; @@ -11,17 +11,27 @@ CodeOceanEditorWebsocket = { // strip anchor if it is in the url sockURL.hash = ''; + sockURL.searchParams.set('HTTP_SENTRY_TRACE', span.toTraceparent()); + const dynamicContext = this.sentryTransaction.getDynamicSamplingContext(); + const baggage = SentryUtils.dynamicSamplingContextToSentryBaggageHeader(dynamicContext); + sockURL.searchParams.set('HTTP_BAGGAGE', baggage); + return sockURL.toString(); }, initializeSocket: function(url) { - this.websocket = new CommandSocket(this.createSocketUrl(url), + const cleanedPath = url.replace(/\/\d+\//, '/*/').replace(/\/[^\/]+$/, '/*'); + const websocketHost = window.location.origin.replace(/^http/, 'ws'); + const sentryDescription = `WebSocket ${websocketHost}${cleanedPath}`; + const span = this.sentryTransaction.startChild({op: 'websocket.client', description: sentryDescription}) + this.websocket = new CommandSocket(this.createSocketUrl(url, span), function (evt) { this.resetOutputTab(); }.bind(this) ); CodeOceanEditorWebsocket.websocket = this.websocket; this.websocket.onError(this.showWebsocketError.bind(this)); + this.websocket.onClose(span.finish.bind(span)); }, initializeSocketForTesting: function(url) { diff --git a/app/assets/javascripts/editor/submissions.js b/app/assets/javascripts/editor/submissions.js index c225cee4..d54011ba 100644 --- a/app/assets/javascripts/editor/submissions.js +++ b/app/assets/javascripts/editor/submissions.js @@ -112,6 +112,7 @@ CodeOceanEditorSubmissions = { }, resetCode: function(initiator, onlyActiveFile = false) { + this.startSentryTransaction(initiator); this.showSpinner(initiator); this.ajax({ method: 'GET', @@ -131,9 +132,11 @@ CodeOceanEditorSubmissions = { }, renderCode: function(event) { + const cause = $('#render'); + this.startSentryTransaction(cause); event.preventDefault(); if ($('#render').is(':visible')) { - this.createSubmission('#render', null, function (response) { + this.createSubmission(cause, null, function (response) { if (response.render_url === undefined) return; const active_file = CodeOceanEditor.active_file.filename.replace(/#$/,''); // remove # if it is the last character, this is not part of the filename and just an anchor @@ -162,10 +165,12 @@ CodeOceanEditorSubmissions = { * Execution-Logic */ runCode: function(event) { + const cause = $('#run'); + this.startSentryTransaction(cause); event.preventDefault(); this.stopCode(event); if ($('#run').is(':visible')) { - this.createSubmission('#run', null, this.runSubmission.bind(this)); + this.createSubmission(cause, null, this.runSubmission.bind(this)); } }, @@ -189,9 +194,11 @@ CodeOceanEditorSubmissions = { }, testCode: function(event) { + const cause = $('#test'); + this.startSentryTransaction(cause); event.preventDefault(); if ($('#test').is(':visible')) { - this.createSubmission('#test', null, function(response) { + this.createSubmission(cause, null, function(response) { this.showSpinner($('#test')); $('#score_div').addClass('d-none'); var url = response.test_url.replace(this.FILENAME_URL_PLACEHOLDER, CodeOceanEditor.active_file.filename.replace(/#$/,'')); // remove # if it is the last character, this is not part of the filename and just an anchor @@ -202,6 +209,7 @@ CodeOceanEditorSubmissions = { submitCode: function(event) { const button = $(event.target) || $('#submit'); + this.startSentryTransaction(button); this.teardownEventHandlers(); this.createSubmission(button, null, function (response) { if (response.redirect) { diff --git a/app/assets/javascripts/editor/websocket.js b/app/assets/javascripts/editor/websocket.js index 088e285f..431ab591 100644 --- a/app/assets/javascripts/editor/websocket.js +++ b/app/assets/javascripts/editor/websocket.js @@ -14,6 +14,10 @@ CommandSocket.prototype.onError = function(callback){ this.websocket.onerror = callback }; +CommandSocket.prototype.onClose = function(callback){ + this.websocket.onclose = callback +}; + /** * Allows it to register an event-handler on the given cmd. * The handler needs to accept one argument, the message. diff --git a/app/javascript/application.js b/app/javascript/application.js index 965ae17a..7e74b856 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -16,11 +16,22 @@ import 'jstree'; import * as _ from 'underscore'; import * as d3 from 'd3'; import * as Sentry from '@sentry/browser'; +import * as SentryIntegration from '@sentry/integrations'; +import { startIdleTransaction, TRACING_DEFAULTS } from '@sentry/core'; +import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import 'sorttable'; window.bootstrap = bootstrap; // Publish bootstrap in global namespace window._ = _; // Publish underscore's `_` in global namespace window.d3 = d3; // Publish d3 in global namespace window.Sentry = Sentry; // Publish sentry in global namespace +window.SentryIntegrations = [ // Publish sentry integration in global namespace + new SentryIntegration.ReportingObserver(), + new SentryIntegration.ExtraErrorData(), + new SentryIntegration.HttpClient(), + new Sentry.BrowserTracing(), + new Sentry.Replay(), +]; +window.SentryUtils = { dynamicSamplingContextToSentryBaggageHeader, startIdleTransaction, TRACING_DEFAULTS }; // CSS import 'chosen-js/chosen.css'; diff --git a/config/application.rb b/config/application.rb index aac487c4..8c05188f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,6 +9,7 @@ require 'rails/all' Bundler.require(*Rails.groups) require 'telegraf/rails' +require_relative '../lib/middleware/websocket_sentry_headers' module CodeOcean class Application < Rails::Application @@ -56,5 +57,8 @@ module CodeOcean # Allow tables in addition to existing default tags config.action_view.sanitized_allowed_tags = ActionView::Base.sanitized_allowed_tags + %w[table thead tbody tfoot td tr] + + # Extract Sentry-related parameters from WebSocket connection + config.middleware.insert_before 0, Middleware::WebSocketSentryHeaders end end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index aff5032b..d650ed5b 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -14,4 +14,5 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'IO' + inflect.acronym 'WebSocket' end diff --git a/lib/middleware/websocket_sentry_headers.rb b/lib/middleware/websocket_sentry_headers.rb new file mode 100644 index 00000000..f2bbd995 --- /dev/null +++ b/lib/middleware/websocket_sentry_headers.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Middleware + class WebSocketSentryHeaders + def initialize(app) + @app = app + end + + def call(env) + request = Rack::Request.new(env) + extract_sentry_parameters(request) if websocket_upgrade?(request) + @app.call(env) + end + + private + + def websocket_upgrade?(request) + request.get_header('HTTP_CONNECTION')&.casecmp?('Upgrade') && + request.get_header('HTTP_UPGRADE')&.casecmp?('websocket') + end + + def extract_sentry_parameters(request) + %w[HTTP_SENTRY_TRACE HTTP_BAGGAGE].each do |param| + request.add_header(param, request.delete_param(param)) + end + end + end +end diff --git a/package.json b/package.json index 2a9ceb2f..51e9e4e9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "@egjs/hammerjs": "^2.0.17", "@fortawesome/fontawesome-free": "^6.4.0", "@popperjs/core": "^2.11.7", - "@sentry/browser": "^7.11.1", + "@sentry/browser": "^7.51.2", + "@sentry/core": "^7.51.2", + "@sentry/integrations": "^7.51.2", + "@sentry/utils": "^7.51.2", "@webpack-cli/serve": "^2.0.3", "babel-loader": "^9.1.2", "bootstrap": "^5.2.3", diff --git a/yarn.lock b/yarn.lock index 093fb86f..df18f6ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1026,57 +1026,67 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.7.tgz#ccab5c8f7dc557a52ca3288c10075c9ccd37fff7" integrity sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw== -"@sentry-internal/tracing@7.47.0": - version "7.47.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.47.0.tgz#45e92eb4c8d049d93bd4fab961eaa38a4fb680f3" - integrity sha512-udpHnCzF8DQsWf0gQwd0XFGp6Y8MOiwnl8vGt2ohqZGS3m1+IxoRLXsSkD8qmvN6KKDnwbaAvYnK0z0L+AW95g== +"@sentry-internal/tracing@7.51.2": + version "7.51.2" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.51.2.tgz#17833047646426ca71445327018ffcb33506a699" + integrity sha512-OBNZn7C4CyocmlSMUPfkY9ORgab346vTHu5kX35PgW5XR51VD2nO5iJCFbyFcsmmRWyCJcZzwMNARouc2V4V8A== dependencies: - "@sentry/core" "7.47.0" - "@sentry/types" "7.47.0" - "@sentry/utils" "7.47.0" + "@sentry/core" "7.51.2" + "@sentry/types" "7.51.2" + "@sentry/utils" "7.51.2" tslib "^1.9.3" -"@sentry/browser@^7.11.1": - version "7.47.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.47.0.tgz#c0d10f348d1fb9336c3ef8fa2f6638f26d4c17a8" - integrity sha512-L0t07kS/G1UGVZ9fpD6HLuaX8vVBqAGWgu+1uweXthYozu/N7ZAsakjU/Ozu6FSXj1mO3NOJZhOn/goIZLSj5A== +"@sentry/browser@^7.51.2": + version "7.51.2" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.51.2.tgz#c01758a54c613be45df58ab503805737256f51a4" + integrity sha512-FQFEaTFbvYHPQE2emFjNoGSy+jXplwzoM/XEUBRjrGo62lf8BhMvWnPeG3H3UWPgrWA1mq0amvHRwXUkwofk0g== dependencies: - "@sentry-internal/tracing" "7.47.0" - "@sentry/core" "7.47.0" - "@sentry/replay" "7.47.0" - "@sentry/types" "7.47.0" - "@sentry/utils" "7.47.0" + "@sentry-internal/tracing" "7.51.2" + "@sentry/core" "7.51.2" + "@sentry/replay" "7.51.2" + "@sentry/types" "7.51.2" + "@sentry/utils" "7.51.2" tslib "^1.9.3" -"@sentry/core@7.47.0": - version "7.47.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.47.0.tgz#6a723d96f64009a9c1b9bc44e259956b7eca0a3f" - integrity sha512-EFhZhKdMu7wKmWYZwbgTi8FNZ7Fq+HdlXiZWNz51Bqe3pHmfAkdHtAEs0Buo0v623MKA0CA4EjXIazGUM34XTg== +"@sentry/core@7.51.2", "@sentry/core@^7.51.2": + version "7.51.2" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.51.2.tgz#f2c938de334f9bf26f4416079168275832423964" + integrity sha512-p8ZiSBxpKe+rkXDMEcgmdoyIHM/1bhpINLZUFPiFH8vzomEr7sgnwRhyrU8y/ADnkPeNg/2YF3QpDpk0OgZJUA== dependencies: - "@sentry/types" "7.47.0" - "@sentry/utils" "7.47.0" + "@sentry/types" "7.51.2" + "@sentry/utils" "7.51.2" tslib "^1.9.3" -"@sentry/replay@7.47.0": - version "7.47.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.47.0.tgz#d2fc8fd3be2360950497426035d1ba0bd8a97b8f" - integrity sha512-BFpVZVmwlezZ83y0L43TCTJY142Fxh+z+qZSwTag5HlhmIpBKw/WKg06ajOhrYJbCBkhHmeOvyKkxX0jnc39ZA== +"@sentry/integrations@^7.51.2": + version "7.51.2" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.51.2.tgz#fce58b9ced601c7f93344b508c67c69a9c883f3d" + integrity sha512-ZnSptbuDQOoQ13mFX9vvLDfXlbMGjenW2fMIssi9+08B7fD6qxmetkYnWmBK+oEipjoGA//0240Fj8FUvZr0Qg== dependencies: - "@sentry/core" "7.47.0" - "@sentry/types" "7.47.0" - "@sentry/utils" "7.47.0" + "@sentry/types" "7.51.2" + "@sentry/utils" "7.51.2" + localforage "^1.8.1" + tslib "^1.9.3" -"@sentry/types@7.47.0": - version "7.47.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.47.0.tgz#fd07dbec11a26ae861532a9abe75bd31663ca09b" - integrity sha512-GxXocplN0j1+uczovHrfkykl9wvkamDtWxlPUQgyGlbLGZn+UH1Y79D4D58COaFWGEZdSNKr62gZAjfEYu9nQA== - -"@sentry/utils@7.47.0": - version "7.47.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.47.0.tgz#e62fdede15e45387b40c9fa135feba48f0960826" - integrity sha512-A89SaOLp6XeZfByeYo2C8Ecye/YAtk/gENuyOUhQEdMulI6mZdjqtHAp7pTMVgkBc/YNARVuoa+kR/IdRrTPkQ== +"@sentry/replay@7.51.2": + version "7.51.2" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.51.2.tgz#1f54e92b472ab87dfdb4e8cd6b8c8252600fe7b0" + integrity sha512-W8YnSxkK9LTUXDaYciM7Hn87u57AX9qvH8jGcxZZnvpKqHlDXOpSV8LRtBkARsTwgLgswROImSifY0ic0lyCWg== dependencies: - "@sentry/types" "7.47.0" + "@sentry/core" "7.51.2" + "@sentry/types" "7.51.2" + "@sentry/utils" "7.51.2" + +"@sentry/types@7.51.2": + version "7.51.2" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.51.2.tgz#cb742f374d9549195f62c462c915adeafed31d65" + integrity sha512-/hLnZVrcK7G5BQoD/60u9Qak8c9AvwV8za8TtYPJDUeW59GrqnqOkFji7RVhI7oH1OX4iBxV+9pAKzfYE6A6SA== + +"@sentry/utils@7.51.2", "@sentry/utils@^7.51.2": + version "7.51.2" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.51.2.tgz#2a52ac2cfb00ffd128248981279c0a561b39eccb" + integrity sha512-EcjBU7qG4IG+DpIPvdgIBcdIofROMawKoRUNKraeKzH/waEYH9DzCaqp/mzc5/rPBhpDB4BShX9xDDSeH+8c0A== + dependencies: + "@sentry/types" "7.51.2" tslib "^1.9.3" "@sinclair/typebox@^0.25.16": @@ -2793,6 +2803,11 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + immutable@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" @@ -3039,6 +3054,13 @@ launch-editor@^2.6.0: picocolors "^1.0.0" shell-quote "^1.7.3" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== + dependencies: + immediate "~3.0.5" + lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -3058,6 +3080,13 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" +localforage@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"