diff --git a/cev_eris.dme b/cev_eris.dme
index 4918ced24a3..7712b0c10b8 100644
--- a/cev_eris.dme
+++ b/cev_eris.dme
@@ -1410,6 +1410,7 @@
#include "code\game\verbs\who.dm"
#include "code\js\byjax.dm"
#include "code\js\menus.dm"
+#include "code\modules\http.dm"
#include "code\modules\aberrants\_defines.dm"
#include "code\modules\aberrants\_modification.dm"
#include "code\modules\aberrants\organs\holders.dm"
@@ -1823,6 +1824,7 @@
#include "code\modules\emoji\emoji_parse.dm"
#include "code\modules\error_handler\error_handler.dm"
#include "code\modules\error_handler\error_viewer.dm"
+#include "code\modules\error_handler\glitchtip.dm"
#include "code\modules\examine\examine.dm"
#include "code\modules\flufftext\Dreaming.dm"
#include "code\modules\flufftext\TextFilters.dm"
@@ -2860,7 +2862,6 @@
#include "code\modules\telesci\circuits.dm"
#include "code\modules\telesci\telepads.dm"
#include "code\modules\telesci\telesci_computer.dm"
-#include "code\modules\text_to_speech\tts_html.dm"
#include "code\modules\text_to_speech\tts_main.dm"
#include "code\modules\tgchat\message.dm"
#include "code\modules\tgchat\to_chat.dm"
diff --git a/code/__HELPERS/time.dm b/code/__HELPERS/time.dm
index 78094643ed6..aac5ec9948a 100644
--- a/code/__HELPERS/time.dm
+++ b/code/__HELPERS/time.dm
@@ -42,6 +42,11 @@ var/next_station_date_change = 1 DAYS
var/time_string = time2text(world.timeofday, format)
return show_ds ? "[time_string]:[world.timeofday % 10]" : time_string
+/proc/time_stamp_metric()
+ var/date_portion = time2text(world.timeofday, "YYYY-MM-DD")
+ var/time_portion = time2text(world.timeofday, "hh:mm:ss")
+ return "[date_portion]T[time_portion]"
+
//Returns the world time in english
/proc/worldtime2text(time = world.time, timeshift = 1)
if(!roundstart_hour) roundstart_hour = rand(0, 23)
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index a9fe52c17b5..9652c5c44d6 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -336,6 +336,27 @@
/datum/config_entry/flag/log_world_topic
+
+/*****************/
+/*ERROR REPORTING*/
+/*****************/
+
+/datum/config_entry/string/glitchtip_dsn
+ config_entry_value = ""
+
+/datum/config_entry/string/glitchtip_dsn/ValidateAndSet(str_val)
+ var/dsn_clean = replacetext(replacetext(str_val, "http://", ""), "https://", "")
+ var/at_pos = findtext(dsn_clean, "@")
+ var/slash_pos = findtext(dsn_clean, "/", at_pos)
+
+ if(!at_pos || !slash_pos)
+ return FALSE
+ return ..()
+
+/datum/config_entry/string/glitchtip_environment
+ config_entry_value = "production"
+
+
/*****************/
/* VOTES */
/*****************/
diff --git a/code/datums/topic/admin.dm b/code/datums/topic/admin.dm
index a8fb33196fb..b6debd60cd3 100644
--- a/code/datums/topic/admin.dm
+++ b/code/datums/topic/admin.dm
@@ -914,14 +914,14 @@
require_perms = list(R_DEBUG)
/datum/admin_topic/viewruntime/Run(list/input)
- var/datum/ErrorViewer/error_viewer = locate(input["viewruntime"])
+ var/datum/error_viewer/error_viewer = locate(input["viewruntime"])
if(!istype(error_viewer))
- to_chat(usr, span_warning("That runtime viewer no longer exists."))
+ to_chat(usr.client, span_warning("That runtime viewer no longer exists."))
return
if(input["viewruntime_backto"])
- error_viewer.showTo(usr, locate(input["viewruntime_backto"]), input["viewruntime_linear"])
+ error_viewer.show_to(usr.client, locate(input["viewruntime_backto"]), input["viewruntime_linear"])
else
- error_viewer.showTo(usr, null, input["viewruntime_linear"])
+ error_viewer.show_to(usr.client, null, input["viewruntime_linear"])
/datum/admin_topic/admincaster
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index 8d16734683d..0462c03ec0e 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -390,7 +390,7 @@
set category = "Debug"
set name = "View Runtimes"
set desc = "Open the Runtime Viewer"
- GLOB.error_cache.showTo(usr)
+ GLOB.error_cache.show_to(src)
/client/proc/spawn_disciple()
set category = "Debug"
diff --git a/code/modules/error_handler/error_handler.dm b/code/modules/error_handler/error_handler.dm
index 621640ef020..258298f1bc2 100644
--- a/code/modules/error_handler/error_handler.dm
+++ b/code/modules/error_handler/error_handler.dm
@@ -4,11 +4,6 @@ GLOBAL_VAR_INIT(total_runtimes_skipped, 0)
#ifdef USE_CUSTOM_ERROR_HANDLER
#define ERROR_USEFUL_LEN 2
-var/regex/stack_workaround = regex("[WORKAROUND_IDENTIFIER](.+?)[WORKAROUND_IDENTIFIER]")
-var/list/error_last_seen = list()
-var/list/error_cooldown = list() /* Error_cooldown items will either be positive(cooldown time) or negative(silenced error)
- If negative, starts at -1, and goes down by 1 each time that error gets skipped*/
-
/world/Error(exception/E, datum/e_src)
GLOB.total_runtimes++
@@ -26,11 +21,16 @@ var/list/error_cooldown = list() /* Error_cooldown items will either be positive
else if(copytext(E.name, 1, 18) == "Out of resources!")//18 == length() of that string + 1
log_world("BYOND out of memory. Restarting ([E?.file]:[E?.line])")
+ // SSplexora.notify_shutdown(PLEXORA_SHUTDOWN_KILLDD)
TgsEndProcess()
. = ..()
Reboot(reason = 1)
return
+ var/static/regex/stack_workaround = regex("[WORKAROUND_IDENTIFIER](.+?)[WORKAROUND_IDENTIFIER]")
+ var/static/list/error_last_seen = list()
+ var/static/list/error_cooldown = list() /* Error_cooldown items will either be positive(cooldown time) or negative(silenced error)
+ If negative, starts at -1, and goes down by 1 each time that error gets skipped*/
if(!error_last_seen) // A runtime is occurring too early in start-up initialization
return ..()
@@ -38,7 +38,7 @@ var/list/error_cooldown = list() /* Error_cooldown items will either be positive
if(!islist(error_last_seen))
return ..() //how the fuck?
- if(stack_workaround?.Find(E.name))
+ if(stack_workaround.Find(E.name))
var/list/data = json_decode(stack_workaround.group[1])
E.file = data[1]
E.line = data[2]
@@ -90,7 +90,7 @@ var/list/error_cooldown = list() /* Error_cooldown items will either be positive
error_cooldown[erroruid] = 0
if(skipcount > 0)
SEND_TEXT(world.log, "\[[time_stamp()]] Skipped [skipcount] runtimes in [E.file],[E.line].")
- GLOB.error_cache.logError(E, skipCount = skipcount)
+ GLOB.error_cache.log_error(E, skip_count = skipcount)
error_last_seen[erroruid] = world.time
error_cooldown[erroruid] = cooldown
@@ -139,7 +139,7 @@ var/list/error_cooldown = list() /* Error_cooldown items will either be positive
if(silencing)
desclines += " (This error will now be silenced for [DisplayTimeText(configured_error_silence_time)])"
if(GLOB.error_cache)
- GLOB.error_cache.logError(E, desclines)
+ GLOB.error_cache.log_error(E, desclines)
var/main_line = "\[[time_stamp()]] Runtime in [E.file],[E.line]: [E]"
SEND_TEXT(world.log, main_line)
@@ -162,6 +162,7 @@ var/list/error_cooldown = list() /* Error_cooldown items will either be positive
"name" = "[E.name]",
"desc" = "[E.desc]"
))
+ send_to_glitchtip(E)
#endif
#undef ERROR_USEFUL_LEN
diff --git a/code/modules/error_handler/error_viewer.dm b/code/modules/error_handler/error_viewer.dm
index d3b17a910e4..8080f3005dd 100644
--- a/code/modules/error_handler/error_viewer.dm
+++ b/code/modules/error_handler/error_viewer.dm
@@ -3,194 +3,193 @@
// There are 3 different types used here:
//
-// - ErrorCache keeps track of all error sources, as well as all individually
+// - error_cache keeps track of all error sources, as well as all individually
// logged errors. Only one instance of this datum should ever exist, and it's
// right here:
-#ifdef DEBUG
-GLOBAL_DATUM_INIT(error_cache, /datum/ErrorViewer/ErrorCache, new)
+#ifdef USE_CUSTOM_ERROR_HANDLER
+GLOBAL_DATUM_INIT(error_cache, /datum/error_viewer/error_cache, new)
#else
// If debugging is disabled, there's nothing useful to log, so don't bother.
-GLOBAL_DATUM(error_cache, /datum/ErrorViewer/ErrorCache)
+GLOBAL_DATUM(error_cache, /datum/error_viewer/error_cache)
#endif
-// - ErrorSource datums exist for each line (of code) that generates an error,
+// - error_source datums exist for each line (of code) that generates an error,
// and keep track of all errors generated by that line.
//
-// - ErrorEntry datums exist for each logged error, and keep track of all
+// - error_entry datums exist for each logged error, and keep track of all
// relevant info about that error.
-// Common vars and procs are kept at the ErrorViewer level
-/datum/ErrorViewer/
+// Common vars and procs are kept at the error_viewer level
+/datum/error_viewer
var/name = ""
-/datum/ErrorViewer/proc/browseTo(user, html)
- if(user)
- var/datum/browser/popup = new(user, "error_viewer", "Runtime Viewer", 700, 500)
- popup.add_head_content({""})
- popup.set_content(html)
- popup.open(0)
-
-/datum/ErrorViewer/proc/buildHeader(datum/ErrorViewer/back_to, linear, refreshable)
- // Common starter HTML for showTo
- var/html = ""
-
- if(istype(back_to))
- html += "[back_to.makeLink("<<<", null, linear)] "
- if(refreshable)
- html += "[makeLink("Refresh", null, linear)]"
- if(html)
- html += "
"
- return html
-
-/datum/ErrorViewer/proc/showTo(user, datum/ErrorViewer/back_to, linear)
+/datum/error_viewer/proc/browse_to(client/user, html)
+ var/datum/browser/browser = new(user.mob, "error_viewer", null, 600, 400)
+ browser.set_content(html)
+ browser.add_head_content({"
+
+ "})
+ browser.open()
+
+/datum/error_viewer/proc/build_header(datum/error_viewer/back_to, linear)
+ // Common starter HTML for show_to
+
+ . = ""
+
+ if (istype(back_to))
+ . += back_to.make_link("<<<", null, linear)
+
+ . += "[make_link("Refresh")]
"
+
+/datum/error_viewer/proc/show_to(user, datum/error_viewer/back_to, linear)
// Specific to each child type
return
-/datum/ErrorViewer/proc/makeLink(linktext, datum/ErrorViewer/back_to, linear)
+/datum/error_viewer/proc/make_link(linktext, datum/error_viewer/back_to, linear)
var/back_to_param = ""
- if(!linktext)
+ if (!linktext)
linktext = name
- if(istype(back_to))
- back_to_param = ";viewruntime_backto=\ref[back_to]"
- if(linear)
+
+ if (istype(back_to))
+ back_to_param = ";viewruntime_backto=[REF(back_to)]"
+
+ if (linear)
back_to_param += ";viewruntime_linear=1"
- return "[html_encode(linktext)]"
-/datum/ErrorViewer/ErrorCache
+ return "[linktext]"
+
+/datum/error_viewer/error_cache
var/list/errors = list()
var/list/error_sources = list()
var/list/errors_silenced = list()
-/datum/ErrorViewer/ErrorCache/showTo(user, datum/ErrorViewer/back_to, linear)
- var/html = buildHeader(null, linear, refreshable=1)
- html += "[GLOB.total_runtimes] runtimes, [GLOB.total_runtimes_skipped] skipped
"
- if(!linear)
- html += "organized | [makeLink("linear", null, 1)]
"
- var/datum/ErrorViewer/ErrorSource/error_source
- for(var/erroruid in error_sources)
+/datum/error_viewer/error_cache/show_to(user, datum/error_viewer/back_to, linear)
+ var/html = build_header()
+ html += "[GLOB.total_runtimes] runtimes, [GLOB.total_runtimes_skipped] skipped
"
+ if (!linear)
+ html += "organized | [make_link("linear", null, 1)]
"
+ var/datum/error_viewer/error_source/error_source
+ for (var/erroruid in error_sources)
error_source = error_sources[erroruid]
- html += "[error_source.makeLink(null, src)]
"
+ html += "[error_source.make_link(null, src)]
"
+
else
- html += "[makeLink("organized", null)] | linear
"
- for(var/datum/ErrorViewer/ErrorEntry/error_entry in errors)
- html += "[error_entry.makeLink(null, src, 1)]
"
- browseTo(user, html)
+ html += "[make_link("organized", null)] | linear
"
+ for (var/datum/error_viewer/error_entry/error_entry in errors)
+ html += "[error_entry.make_link(null, src, 1)]
"
+
+ browse_to(user, html)
-/datum/ErrorViewer/ErrorCache/proc/logError(exception/e, list/desclines, skipCount, datum/e_src)
- if(!istype(e))
+/datum/error_viewer/error_cache/proc/log_error(exception/e, list/desclines, skip_count)
+ if (!istype(e))
return // Abnormal exception, don't even bother
var/erroruid = "[e.file][e.line]"
- var/datum/ErrorViewer/ErrorSource/error_source = error_sources[erroruid]
- if(!error_source)
+ var/datum/error_viewer/error_source/error_source = error_sources[erroruid]
+ if (!error_source)
error_source = new(e)
error_sources[erroruid] = error_source
- var/datum/ErrorViewer/ErrorEntry/error_entry = new(e, desclines, skipCount, e_src)
+ var/datum/error_viewer/error_entry/error_entry = new(e, desclines, skip_count)
error_entry.error_source = error_source
errors += error_entry
error_source.errors += error_entry
- if(skipCount)
- return // Skip notifying admins about skipped errors
+ if (skip_count)
+ return // Skip notifying admins about skipped errors.
// Show the error to admins with debug messages turned on, but only if one
// from the same source hasn't been shown too recently
- if(error_source.next_message_at <= world.time)
+ if (error_source.next_message_at <= world.time)
var/const/viewtext = "\[view]" // Nesting these in other brackets went poorly
- log_debug("Runtime in [e.file],[e.line]: [html_encode(e.name)] [error_entry.makeLink(viewtext)]")
- error_source.next_message_at = world.time + ERROR_MSG_DELAY
-
-/datum/ErrorViewer/ErrorSource
+ //log_debug("Runtime in [e.file], line [e.line]: [html_encode(e.name)] [error_entry.make_link(viewtext)]")
+ var/err_msg_delay
+ if(config?.loaded)
+ err_msg_delay = CONFIG_GET(number/error_msg_delay)
+ else
+ var/datum/config_entry/CE = /datum/config_entry/number/error_msg_delay
+ err_msg_delay = initial(CE.default)
+ error_source.next_message_at = world.time + err_msg_delay
+
+/datum/error_viewer/error_source
var/list/errors = list()
var/next_message_at = 0
-/datum/ErrorViewer/ErrorSource/New(exception/e)
- if(!istype(e))
+/datum/error_viewer/error_source/New(exception/e)
+ if (!istype(e))
name = "\[[time_stamp()]] Uncaught exceptions"
return
- name = "\[[time_stamp()]] Runtime in [e.file],[e.line]: [e]"
-/datum/ErrorViewer/ErrorSource/showTo(user, datum/ErrorViewer/back_to, linear)
- if(!istype(back_to))
+ name = "\[[time_stamp()]] Runtime in [e.file], line [e.line]: [html_encode(e.name)]"
+
+/datum/error_viewer/error_source/show_to(user, datum/error_viewer/back_to, linear)
+ if (!istype(back_to))
back_to = GLOB.error_cache
- var/html = buildHeader(back_to, refreshable=1)
- for(var/datum/ErrorViewer/ErrorEntry/error_entry in errors)
- html += "[error_entry.makeLink(null, src)]
"
- browseTo(user, html)
-/datum/ErrorViewer/ErrorEntry
- var/datum/ErrorViewer/ErrorSource/error_source
+ var/html = build_header(back_to)
+ for (var/datum/error_viewer/error_entry/error_entry in errors)
+ html += "[error_entry.make_link(null, src)]
"
+
+ browse_to(user, html)
+
+/datum/error_viewer/error_entry
+ var/datum/error_viewer/error_source/error_source
var/exception/exc
var/desc = ""
- var/srcRef
- var/srcType
- var/turf/srcLoc
- var/usrRef
- var/turf/usrLoc
- var/isSkipCount
-
-/datum/ErrorViewer/ErrorEntry/New(exception/e, list/desclines, skipCount, datum/e_src)
- if(!istype(e))
- name = "\[[time_stamp()]] Uncaught exception: [e]"
+ var/usr_ref
+ var/turf/usr_loc
+ var/is_skip_count
+
+/datum/error_viewer/error_entry/New(exception/e, list/desclines, skip_count)
+ if (!istype(e))
+ name = "\[[time_stamp()]] Uncaught exception: [html_encode(e.name)]"
return
- if(skipCount)
- name = "\[[time_stamp()]] Skipped [skipCount] runtimes in [e.file],[e.line]."
- isSkipCount = TRUE
+
+ if(skip_count)
+ name = "\[[time_stamp()]] Skipped [skip_count] runtimes in [e.file],[e.line]."
+ is_skip_count = TRUE
return
- name = "\[[time_stamp()]] Runtime in [e.file],[e.line]: [e]"
+
+ name = "\[[time_stamp()]] Runtime in [e.file], line [e.line]: [html_encode(e.name)]"
exc = e
- if(istype(desclines))
- for(var/line in desclines)
+ if (istype(desclines))
+ for (var/line in desclines)
// There's probably a better way to do this than non-breaking spaces...
- desc += " " + html_encode(line) + "
"
- if(istype(e_src))
- srcRef = "\ref[e_src]"
- srcType = e_src.type
- srcLoc = get_turf(e_src)
- if(usr)
- usrRef = "\ref[usr]"
- usrLoc = get_turf(usr)
-
-/datum/ErrorViewer/ErrorEntry/showTo(user, datum/ErrorViewer/back_to, linear)
- if(!istype(back_to))
+ desc += "[html_encode(line)]
"
+
+ if (usr)
+ usr_ref = "[REF(usr)]"
+ usr_loc = get_turf(usr)
+
+/datum/error_viewer/error_entry/show_to(user, datum/error_viewer/back_to, linear)
+ if (!istype(back_to))
back_to = error_source
- var/html = buildHeader(back_to, linear)
- html += "[html_encode(name)]
[desc]
"
- if(srcRef)
- html += "
src: VV"
- if(ispath(srcType, /mob))
- html += " [ADMIN_PP(srcRef)]"
- html += " [ADMIN_FLW(srcRef)]"
- if(istype(srcLoc))
- html += "
src.loc: [ADMIN_VV(srcLoc)]"
- html += " [ADMIN_JMP(srcLoc)]"
- if(usrRef)
- html += "
usr: [ADMIN_VV(usrRef)]"
- html += " [ADMIN_PP(usrRef)]"
- html += " [ADMIN_FLW(usrRef)]"
- if(istype(usrLoc))
- html += "
usr.loc: VV"
- html += " [ADMIN_JMP(usrLoc)]"
- browseTo(user, html)
-
-/datum/ErrorViewer/ErrorEntry/makeLink(linktext, datum/ErrorViewer/back_to, linear)
- if(isSkipCount)
- return html_encode(name)
- return ..()
+
+ var/html = build_header(back_to, linear)
+ html += "[name][desc]
"
+ if (usr_ref)
+ html += "
usr: VV"
+ html += " PP"
+ html += " Follow"
+ if (istype(usr_loc))
+ html += "
usr.loc: VV"
+ html += " JMP"
+
+ browse_to(user, html)
+
+/datum/error_viewer/error_entry/make_link(linktext, datum/error_viewer/back_to, linear)
+ return is_skip_count ? name : ..()
diff --git a/code/modules/error_handler/glitchtip.dm b/code/modules/error_handler/glitchtip.dm
new file mode 100644
index 00000000000..592ed792e47
--- /dev/null
+++ b/code/modules/error_handler/glitchtip.dm
@@ -0,0 +1,239 @@
+// This might be compatible with sentry, I'm not sure, my trial period expired so I can't test lol
+// Configuration options are in entries/general.dm
+
+/proc/send_to_glitchtip(exception/E, list/extra_data = null)
+ #ifndef SPACEMAN_DMM
+ #ifndef OPENDREAM
+ if(!CONFIG_GET(string/glitchtip_dsn))
+ return
+ var/glitchtip_dsn = CONFIG_GET(string/glitchtip_dsn)
+
+ // parse DSN to get the key, host and project id
+ // Format: https://key@host/project_id
+ var/dsn_clean = replacetext(replacetext(glitchtip_dsn, "http://", ""), "https://", "")
+ var/at_pos = findtext(dsn_clean, "@")
+ var/slash_pos = findtext(dsn_clean, "/", at_pos)
+ if(!at_pos || !slash_pos)
+ log_runtime("Invalid Glitchtip DSN format")
+ return
+ var/key = copytext(dsn_clean, 1, at_pos)
+ var/host = copytext(dsn_clean, at_pos + 1, slash_pos)
+ var/project_id = copytext(dsn_clean, slash_pos + 1)
+
+ var/list/event_data = list()
+ event_data["event_id"] = rustg_generate_uuid_v4()
+ event_data["timestamp"] = time_stamp_metric()
+ event_data["level"] = "error"
+ event_data["platform"] = world.system_type
+ event_data["server_name"] = world.name
+ event_data["environment"] = CONFIG_GET(string/glitchtip_environment)
+
+ event_data["sdk"] = list(
+ "name" = "byond-glitchtip",
+ "version" = "1.0.0"
+ )
+
+ var/list/exception_data = list()
+ exception_data["type"] = "BYOND Runtime Error"
+ exception_data["value"] = E.name
+ exception_data["module"] = E.file
+
+ // Build stack trace using caller/callee chain
+ var/list/frames = list()
+
+ // Add the error location as the first frame
+ var/list/error_frame = list()
+ error_frame["filename"] = E.file || "unknown"
+ error_frame["lineno"] = E.line || 0
+ error_frame["function"] = "runtime_error"
+ error_frame["in_app"] = TRUE
+ frames += list(error_frame)
+
+ // Walk the call stack using callee objects
+ var/frame_count = 0
+ var/max_frames = 50 // Prevent infinite loops or excessive data
+ for(var/callee/p = caller; p && frame_count < max_frames; p = p.caller)
+ frame_count++
+ var/proc_name = "unknown"
+ var/file_name = "unknown"
+ var/line_num = 0
+
+ if(p.proc)
+ proc_name = "[p.proc.type]"
+ // Clean up the proc name if it has path separators
+ var/slash_pos_inner = findtext(proc_name, "/", -1)
+ if(slash_pos_inner && slash_pos_inner < length(proc_name))
+ proc_name = copytext(proc_name, slash_pos_inner + 1)
+
+ // Get file and line information if available
+ if(p.file)
+ file_name = p.file
+ line_num = p.line || 0
+
+ if(findtext(file_name, "master.dm") && (proc_name == "Loop" || proc_name == "StartProcessing"))
+ break
+
+ var/list/frame = list()
+ frame["filename"] = file_name
+ frame["lineno"] = line_num
+ frame["function"] = proc_name
+ frame["in_app"] = TRUE
+
+ // Collect all available variables for this frame
+ var/list/frame_vars = list()
+
+ // Add context variables
+ if(p.src)
+ frame_vars["src"] = "[p.src]"
+ if(p.usr)
+ frame_vars["usr"] = "[p.usr]"
+
+ // Add procedure arguments
+ if(p.args && length(p.args))
+ for(var/i = 1; i <= length(p.args); i++)
+ var/datum/arg_value = p.args[i]
+ var/arg_string = "null"
+
+ // Not so sanely convert argument to string representation
+ try
+ if(arg_value == null)
+ arg_string = "null"
+ else if(isnum(arg_value))
+ arg_string = "[arg_value]"
+ else if(istext(arg_value))
+ // URL decode if it looks like URL-encoded data
+ var/decoded_value = arg_value
+ if(findtext(arg_value, "%") || findtext(arg_value, "&") || findtext(arg_value, "="))
+ decoded_value = url_decode(arg_value)
+
+ if(length(decoded_value) > 200)
+ arg_string = "\"[copytext(decoded_value, 1, 198)]...\""
+ else
+ arg_string = "\"[decoded_value]\""
+ else if(islist(arg_value))
+ // Handle lists by showing summary and contents
+ var/list/L = arg_value
+ if(length(L) == 0)
+ arg_string = "list(empty)"
+ else
+ arg_string = "list([length(L)] items)"
+
+ // Build contents string
+ var/list/content_items = list()
+ var/max_list_items = 20 // Prevent too long contents
+ var/items_to_show = min(length(L), max_list_items)
+
+ for(var/j = 1; j <= items_to_show; j++)
+ var/datum/item = L[j]
+ var/item_string = "null"
+
+ try
+ if(item == null)
+ item_string = "null"
+ else if(isnum(item))
+ item_string = "[item]"
+ else if(istext(item))
+ // URL decode as a treat
+ var/decoded_item = item
+ if(findtext(item, "%") || findtext(item, "&") || findtext(item, "="))
+ decoded_item = url_decode(item)
+
+ if(length(decoded_item) > 50)
+ item_string = "\"[copytext(decoded_item, 1, 48)]...\""
+ else
+ item_string = "\"[decoded_item]\""
+ else if(istype(item))
+ var/item_type_name = "[item.type]"
+ var/slash_pos_item = findtext(item_type_name, "/", -1)
+ if(slash_pos_item && slash_pos_item < length(item_type_name))
+ item_type_name = copytext(item_type_name, slash_pos_item + 1)
+ item_string = "[item_type_name]([item])"
+ else
+ item_string = "[item]"
+ catch
+ item_string = ""
+
+ content_items += item_string
+
+ var/contents_string = jointext(content_items, ", ")
+ if(length(L) > max_list_items)
+ contents_string += ", ... and [length(L) - max_list_items] more"
+
+ frame_vars["arg[i]_contents"] = contents_string
+ else if(istype(arg_value))
+ var/type_name = "[arg_value.type]"
+ var/slash_pos_obj = findtext(type_name, "/", -1)
+ if(slash_pos_obj && slash_pos_obj < length(type_name))
+ type_name = copytext(type_name, slash_pos_obj + 1)
+ arg_string = "[type_name]: [arg_value]"
+ else
+ arg_string = "[arg_value]"
+ catch
+ arg_string = ""
+
+ frame_vars["arg[i]"] = arg_string
+
+ if(length(frame_vars))
+ frame["vars"] = frame_vars
+
+
+ frames += list(frame)
+
+ exception_data["stacktrace"] = list("frames" = frames)
+ event_data["exception"] = list("values" = list(exception_data))
+
+ // User context
+ if(istype(usr))
+ var/list/user_data = list()
+ user_data["key"] = usr.key
+ user_data["character_name"] = usr.name
+ user_data["character_realname"] = usr.real_name
+ user_data["character_mobtype"] = usr.type
+ user_data["character_job"] = usr.GetJob()
+ if(usr.client)
+ user_data["byond_version"] = usr.client.byond_version
+ user_data["byond_build"] = usr.client.byond_build
+ // user_data["ip_address"] = usr.client.address
+ // user_data["computer_id"] = usr.client.computer_id
+ user_data["holder"] = usr.client.holder?.name
+ event_data["user"] = user_data
+
+ // Add location context
+ var/locinfo = loc_name(usr)
+ if(locinfo)
+ if(!extra_data)
+ extra_data = list()
+ extra_data["user_location"] = locinfo
+
+ if(extra_data)
+ event_data["extra"] = extra_data
+
+ // Tags for filtering in Glitchtip
+ event_data["tags"] = list(
+ "round_id" = GLOB.round_id,
+ "file" = E.file,
+ "line" = "[E.line]",
+ "byond_version" = DM_VERSION,
+ "byond_build" = DM_BUILD,
+ )
+
+ event_data["fingerprint"] = list("[E.file]:[E.line]", E.name)
+
+ send_glitchtip_request(event_data, host, project_id, key)
+ #endif
+ #endif
+
+/proc/send_glitchtip_request(list/event_data, host, project_id, key)
+ var/glitchtip_url = "https://[host]/api/[project_id]/store/"
+ var/json_payload = json_encode(event_data)
+
+ // Glitchtip/Sentry auth header - According to docs this needs to be like this
+ var/auth_header = "Sentry sentry_version=7, sentry_client=byond-glitchtip/1.0.0, sentry_key=[key], sentry_timestamp=[time_stamp_metric()]"
+
+ var/datum/http_request/request = new()
+ request.prepare(RUSTG_HTTP_METHOD_POST, glitchtip_url, json_payload, list(
+ "X-Sentry-Auth" = auth_header,
+ "Content-Type" = "application/json",
+ "User-Agent" = get_useragent("Glitchtip-Implementation")
+ ))
+ request.fire_and_forget()
diff --git a/code/modules/http.dm b/code/modules/http.dm
new file mode 100644
index 00000000000..cfc0d5615f7
--- /dev/null
+++ b/code/modules/http.dm
@@ -0,0 +1,121 @@
+/datum/http_request
+ var/id
+ var/in_progress = FALSE
+
+ var/method
+ var/body
+ var/headers
+ var/url
+ /// If present response body will be saved to this file.
+ var/output_file
+
+ var/_raw_response
+
+/datum/http_request/New(...)
+ . = ..()
+ if(length(args))
+ src.prepare(arglist(args))
+
+/datum/http_request/proc/prepare(method, url, body = "", list/headers, output_file)
+ if (!length(headers))
+ headers = json_encode(list("User-Agent" = get_useragent()))
+ else
+ if (!headers["User-Agent"])
+ headers["User-Agent"] = get_useragent()
+ headers = json_encode(headers)
+
+ src.method = method
+ src.url = url
+ src.body = body
+ src.headers = headers
+ src.output_file = output_file
+
+/datum/http_request/proc/fire_and_forget()
+ var/result = rustg_http_request_fire_and_forget(method, url, body, headers, build_options())
+ if(result != "ok")
+ CRASH("[result]")
+
+/datum/http_request/proc/execute_blocking()
+ _raw_response = rustg_http_request_blocking(method, url, body, headers, build_options())
+
+/datum/http_request/proc/begin_async()
+ if (in_progress)
+ CRASH("Attempted to re-use a request object.")
+
+ id = rustg_http_request_async(method, url, body, headers, build_options())
+
+ if (isnull(text2num(id)))
+ stack_trace("Proc error: [id]")
+ _raw_response = "Proc error: [id]"
+ else
+ in_progress = TRUE
+
+/datum/http_request/proc/build_options()
+ if(output_file)
+ return json_encode(list("output_filename"=output_file,"body_filename"=null))
+ return null
+
+/datum/http_request/proc/is_complete()
+ if (isnull(id))
+ return TRUE
+
+ if (!in_progress)
+ return TRUE
+
+ var/r = rustg_http_check_request(id)
+
+ if (r == RUSTG_JOB_NO_RESULTS_YET)
+ return FALSE
+ else
+ _raw_response = r
+ in_progress = FALSE
+ return TRUE
+
+/datum/http_request/proc/into_response()
+ var/datum/http_response/R = new()
+
+ try
+ var/list/L = json_decode(_raw_response)
+ R.status_code = L["status_code"]
+ R.headers = L["headers"]
+ R.body = L["body"]
+ catch
+ R.errored = TRUE
+ R.error = _raw_response
+
+ return R
+
+/datum/http_response
+ var/status_code
+ var/body
+ var/list/headers
+
+ var/errored = FALSE
+ var/error
+
+/**
+ * Returns a user-agent for http(s) requests
+ * * comment - {str || list} String or list, comments to be applied to the user-agent
+ *
+ * ```
+ * // returns `BYOND 516.1666 ss13-monkestation/deadbeef (Comment-One; Comment-Two)`
+ * get_useragent(list("Comment-One", "Comment-Two"))
+ * // returns `BYOND 516.1666 ss13-monkestation/deadbeef (My-comment)`
+ * get_useragent("My-comment")
+ * ```
+ */
+/proc/get_useragent(comment)
+ . = "BYOND/[DM_VERSION].[DM_BUILD] ss13-monkestation/[copytext(GLOB.revdata.commit, 0, 8) || "NOCOMMIT"] "
+
+ if (istext(comment))
+ . += " ([comment])"
+ else if (islist(comment))
+ var/list/comments = comment
+ if (length(comments))
+ . += " ("
+ for (var/i = 1; i <= length(comments); i++)
+ . += "[comments[i]]"
+ if (i == length(comments))
+ . += ")"
+ break
+ . += ";"
diff --git a/code/modules/text_to_speech/tts_html.dm b/code/modules/text_to_speech/tts_html.dm
deleted file mode 100644
index b24b9148f2f..00000000000
--- a/code/modules/text_to_speech/tts_html.dm
+++ /dev/null
@@ -1,81 +0,0 @@
-/datum/http_request
- var/id
- var/in_progress = FALSE
- var/method
- var/body
- var/headers
- var/url
- /// If present response body will be saved to this file.
- var/output_file
- var/_raw_response
-
-
-/datum/http_request/proc/prepare(_method, _url, _body = "", list/_headers, _output_file)
- headers = LAZYLEN(_headers) ? json_encode(_headers) : ""
- method = _method
- url = _url
- body = _body
- output_file = _output_file
-
-
-/datum/http_request/proc/execute_blocking()
- _raw_response = rustg_http_request_blocking(method, url, body, headers, build_options())
-
-
-/datum/http_request/proc/begin_async()
- if(in_progress)
- CRASH("Attempted to re-use a request object.")
-
- id = rustg_http_request_async(method, url, body, headers, build_options())
-
- if(isnull(text2num(id)))
- stack_trace("Proc error: [id]")
- _raw_response = "Proc error: [id]"
- else
- in_progress = TRUE
-
-
-/datum/http_request/proc/build_options()
- if(output_file)
- return json_encode(list("output_filename" = output_file, "body_filename" = null))
- return null
-
-
-/datum/http_request/proc/is_complete()
- if(isnull(id))
- return TRUE
-
- if(!in_progress)
- return TRUE
-
- var/r = rustg_http_check_request(id)
-
- if(r == RUSTG_JOB_NO_RESULTS_YET)
- return FALSE
- else
- _raw_response = r
- in_progress = FALSE
- return TRUE
-
-
-/datum/http_request/proc/into_response()
- var/datum/http_response/R = new()
-
- try
- var/list/L = json_decode(_raw_response)
- R.status_code = L["status_code"]
- R.headers = L["headers"]
- R.body = L["body"]
- catch
- R.errored = TRUE
- R.error = _raw_response
-
- return R
-
-
-/datum/http_response
- var/status_code
- var/body
- var/list/headers
- var/errored = FALSE
- var/error
diff --git a/config/example/config.txt b/config/example/config.txt
index 1e1d5abda37..501f64ccf0d 100644
--- a/config/example/config.txt
+++ b/config/example/config.txt
@@ -368,6 +368,11 @@ CONFIG_ERRORS_RUNTIME
##How long to wait between messaging admins about occurences of a unique error
#ERROR_MSG_DELAY 50
+## Error reporting
+# Your Sentry/Glitchtip DSN url
+#GLITCHTIP_DSN
+# Environment this is running in, "production", "development" and "testing" are most common
+#GLITCHTIP_ENVIRONMENT development
## Uncomment to allow admins with +DEBUG to start the byond-tracy profiler during the round.
#ALLOW_TRACY_START