Skip to main content

Crash reporting

Games send crashes to https://api.blazium.online/api/v1/public/crashes. Reports appear on your game's dashboard and through the MCP tools list_game_crashes and get_crash.

Crash ingest needs no secret. It identifies the game with two headers:

HeaderValue
X-App-IdThe game uid
X-Build-IdThe build_id of the exact build that crashed (not a version string)

Get both from get_deploy_info (auth.crash_headers) or list_game_builds. Each version, OS, arch, and channel combination is its own build, so write the matching build_id into each exported binary.

Blazium Engine​

Set these in Project Settings under application/crash_reporter/:

SettingValue
enabledtrue
upload_modeInEngine, Sidecar, or Both
endpointhttps://api.blazium.online/api/v1/public/crashes
app_idThe game uid
build_idThe build UID
app_versionThe build version
build_channelstable, beta, and so on
require_user_consenttrue (recommended)
privacy_policy_urlYour privacy policy

For the sidecar consent UI, place the crash reporter binary next to the game executable. Nothing is uploaded until the player confirms. Never put secrets in Project Settings.

Custom reporter​

Create the report:

POST https://api.blazium.online/api/v1/public/crashes
X-App-Id: <game uid>
X-Build-Id: <build_id>
Content-Type: application/json

{ "has_dump": true, "has_log": true, "app_name": "My Game", "app_version": "1.0.0",
"engine_version": "4.8", "os": "windows", "arch": "x86_64",
"user_message": "It froze on level 3", "anonymous": true, "metadata": {} }

The 201 response contains the report id and an uploads object with a dump and/or log entry:

FieldMeaning
urlSingle-use upload URL
methodPUT
expires_at24 hours after creation
max_bytes64 MB
filenamedump.dmp or log.txt

Upload each file with PUT to its url, as the raw body or a multipart file part. Each game has a daily limit on upload URLs. Past it, the report is still stored but uploads is empty.

CodeMeaning
4001Invalid JSON body
4010Missing X-App-Id or X-Build-Id
4030Unknown app id, or a build_id that does not belong to that app
4130metadata has more than 64 keys (413)
4290The game reached its daily crash report limit (429); try again tomorrow

Custom events​

POST https://api.blazium.online/api/v1/public/events
X-App-Id: <game uid>
X-Build-Id: <build_id>
Content-Type: application/json

{ "events": [ { "event": "level_complete", "anonymous": true, "device_uid": "<random per install>" } ] }

Send up to 100 events per request; more returns 4130 (413). The response is 202. Use a random per-install id, not a hardware identifier.

Standard events​

Six event names have fixed meanings. Send them from every build and your listing gets a launch health band, and new games can appear on the Unheard of shelf.

EventWhenFields
session_startThe game process started
boot_okThe first frame of the main menu or first scene rendereddevice_uid (required), ms since start (optional, 0 to 600000)
first_inputThe player's first key, click or touchms since start (optional, 0 to 600000)
session_endThe game is closing normallyseconds played this session (required, whole number 0 to 86400)
quitThe player chose to quit
crashSent by the crash reporter, or on the next launch after an unclean exitdevice_uid (required)

If any standard event in a request has a missing or out-of-range field, the whole request is refused with 4158 and the message names the event's index. Other event names are stored as custom events.

A crash before a device's first boot_ok counts as a crash on boot. Health counts devices, so send the same random device_uid from every event of one install.

A Blazium or Godot autoload can send them:

extends Node

const ENDPOINT := "https://api.blazium.online/api/v1/public/events"
const APP_ID := "<game uid>"
const BUILD_ID := "<build_id>"

var _device_uid := ""
var _started_ms := 0
var _input_sent := false

func _ready() -> void:
_started_ms = Time.get_ticks_msec()
_device_uid = _load_device_uid()
_send("session_start")
await get_tree().process_frame
_send("boot_ok", {"ms": _elapsed_ms()})

func _input(event: InputEvent) -> void:
if _input_sent or not (event is InputEventKey or event is InputEventMouseButton or event is InputEventScreenTouch or event is InputEventJoypadButton):
return
_input_sent = true
_send("first_input", {"ms": _elapsed_ms()})

func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST:
_send("session_end", {"seconds": _session_seconds()})

func quit_game() -> void:
_send("quit")
_send("session_end", {"seconds": _session_seconds()})
get_tree().quit()

func _elapsed_ms() -> int:
return mini(Time.get_ticks_msec() - _started_ms, 600000)

func _session_seconds() -> int:
return mini((Time.get_ticks_msec() - _started_ms) / 1000, 86400)

func _load_device_uid() -> String:
var path := "user://device_uid"
if FileAccess.file_exists(path):
return FileAccess.get_file_as_string(path).strip_edges()
var uid := Crypto.new().generate_random_bytes(16).hex_encode()
FileAccess.open(path, FileAccess.WRITE).store_string(uid)
return uid

func _send(name: String, fields := {}) -> void:
var ev := {"event": name, "anonymous": true, "device_uid": _device_uid}
ev.merge(fields)
var http := HTTPRequest.new()
add_child(http)
http.request_completed.connect(func(_r, _c, _h, _b): http.queue_free())
http.request(ENDPOINT, ["Content-Type: application/json", "X-App-Id: " + APP_ID, "X-Build-Id: " + BUILD_ID],
HTTPClient.METHOD_POST, JSON.stringify({"events": [ev]}))

Set BUILD_ID for each export (see CI). Requests sent while the window is closing may not finish; set get_tree().auto_accept_quit = false and quit after the request completes if you need every session_end. Ask players for consent where your privacy policy requires it.

Symbols​

Reports with a minidump are stackwalked with the Breakpad symbols uploaded for their build. Without symbols, stacks only show addresses. Upload them with chauffeur symbols; see Symbols.

Reading crashes​

With the MCP server connected, ask your agent to list recent crashes. It uses:

  • list_crash_groups for reports grouped by cause, most recently seen first (up to 100 groups), with counts per build
  • list_game_crashes for recent reports
  • list_bug_tickets for bug reports players filed, with a crash_id when they attached a dump or log
  • update_bug_ticket to mark a player bug report fixed or closed, or reopen it with open
  • get_crash for metadata, the player's message, and the stack excerpt
  • request_crash_download with kind stack, log, or dump for a private link valid for 1 hour

Reports with a minidump are stackwalked on the server, and grouped by their top stack frames. Until then they are grouped by the crash message, app version, and OS. A new report can take a few minutes to join a group.

In Cursor, the blazium-games-debug-crash skill runs this whole flow and maps the stack to your code.