betaNew accounts get 7 days of the Server plan, free. No card. what is in it

[ 0.00]

[script:fivetrace] docs · error tracking

Installing the SDK

Two lines in your manifest and one call. Five minutes, including reading this.

>1 · add the resource to the server
[ 0.00]

Unzip fivetrace-sdk into your resources folder, keeping the folder name, and ensure it before anything that uses it. The include will not resolve if it starts later.

Download fivetrace-sdk 0.2.0zip · fivetrace.com/sdk/download always serves the current one

Shipping a script to customers? Point them at that URL rather than bundling a copy. It always serves the current SDK, and a bundled one becomes the version your customers are stuck on.

# server.cfg
ensure fivetrace-sdk
ensure my-awesome-script
>2 · load it into your resource
[ 0.11]

This is the part people get wrong. The SDK is not a resource that watches yours from outside — FiveM isolates every resource’s Lua state, so that cannot work. Your resource loads the file into its own state:

-- my-awesome-script/fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

shared_script '@fivetrace-sdk/init.lua'
dependency  'fivetrace-sdk'

server_scripts { 'server/main.lua' }
>3 · initialise it
[ 0.14]

Call this once, as early as you can. Your DSN is on the script’s page in the dashboard.

-- my-awesome-script/server/main.lua
FiveTrace.init({ dsn = 'ft_your_key_here' })
>4 · check it works
[ 0.15]

A correct install and a missing one look identical until something breaks: an empty dashboard either way. So send one on purpose. It goes down the same path a real error does, which is the only way the check means anything.

-- temporarily, then remove it
FiveTrace.test()

It shows up as an ordinary issue within a few seconds. Resolve it and drop the line.

>what gets captured

Unhandled errors thrown inside CreateThread, SetTimeout and AddEventHandler callbacks, with the message, the file and line, and a multi-frame stack. For anything outside those entry points, report it yourself:

xpcall(riskyThing, FiveTrace.capture)

xpcall rather than pcall, and the difference is larger than it looks. The stack is read at the moment capture runs. A pcall has already returned by then, so the frames that failed are gone and what arrives is the line that reported it, with nothing underneath. An xpcall message handler runs with those frames still on the stack, which is why the SDK wraps its own entry points that way.

It also decides how things group. An issue is its resource, its message with the numbers and strings taken out, and its file and line — so with pcall every hand-reported error in a resource carries the same file and line, and two unrelated bugs reported through one call site arrive as one issue with a misleading count.

>where your code actually runs

Those three entry points cover a script written in plain FiveM. A script written against a framework is mostly none of them: the code that runs is a callback the framework registered, an export another resource calls, or a command. Nothing of ours sits between those and an error, so the one place a customer’s server differs from yours is the place you hear nothing from.

One helper fixes all of them. It is the same shape as the wrapping the SDK does for itself, including the console line: the person running the server did not install this, and a console that goes quiet is worse for them than the error was.

-- Once, near the top of your server file.
local function wrap(fn)
  return function(...)
    local function onError(err)
      FiveTrace.capture(err)

      -- The line the server owner would have seen without us. They did not
      -- install this and a console that goes quiet is worse for them than
      -- the error was.
      print('^1[my-script]^7 ' .. tostring(err))

      return err
    end

    local results = table.pack(xpcall(fn, onError, ...))
    if results[1] then return table.unpack(results, 2, results.n) end
  end
end
[ 00.21]

[callbacks] where a framework script spends its time

-- ESX
ESX.RegisterServerCallback('my-script:getThing', wrap(function(src, cb, id)
  cb(lookUp(id))
end))

-- QBCore
QBCore.Functions.CreateCallback('my-script:getThing', wrap(function(source, cb)
  cb(lookUp(source))
end))

-- ox_lib, which is what Qbox uses
lib.callback.register('my-script:getThing', wrap(function(source, id)
  return lookUp(id)
end))

Wrapping changes nothing about how they are called. ESX and QBCore hand your function a reply function to call; ox_lib takes the value you return, which is why the helper passes return values back out rather than swallowing them.

[ 00.22]

[exports · commands] the two people forget

exports('getThing', wrap(function(id)
  return lookUp(id)
end))

RegisterCommand('mything', wrap(function(source, args)
  doTheThing(source, args[1])
end), false)

An export is called by somebody else’s resource, so an error inside yours surfaces as a fault in theirs. Wrapping it means you get the issue, with your file and your line, rather than a support message about a script you did not write.

>ten places your code runs
CreateThread

Wrapped by init. An error in a thread you started is captured with its stack, and one line reaches the server owner’s console the way it always did.

SetTimeout

Wrapped by init, for the same reason and in the same way.

AddEventHandler

Wrapped by init. This is the one that makes a plain FiveM script mostly covered without doing anything.

ESX.RegisterServerCallback

Registered with the framework rather than with AddEventHandler, so nothing of ours is between it and the error. The callback takes the source first and the reply function second, and wrapping does not change either.

QBCore.Functions.CreateCallback

The same, with the source and the reply function the other way round in your own handler’s body. Qbox servers mostly use the ox_lib form below instead.

lib.callback.register

ox_lib callbacks return their value rather than calling a reply function, which is why the helper above passes return values back rather than swallowing them.

exports

An export is called by another resource, and an error inside yours surfaces in theirs. Wrapping it means the issue lands in your dashboard with your file and line rather than as a complaint about somebody else’s script.

RegisterCommand

Commands are the entry point most often forgotten, because they are usually the admin tooling rather than the product, and an admin command that throws is the one nobody hears about.

ox_inventory hooks, ox_target actions

Your handler, invoked by their resource. Whether it arrives inside anything we wrapped depends on how they scheduled it, which is not ours to promise. Wrap it, or use the test below.

oxmysql callbacks

Same answer, and worth caring about more: a query callback is where a script touches data it does not control, which is where a customer’s server differs from yours.

Where the table says it depends, do not take our word for it in either direction. Put a deliberate error inside the handler, restart the resource, and look. If it arrives, that entry point was already covered and the wrap comes off. If it does not, leave it on. The question is answered in ten seconds and stays answered for that version of that resource.

error('checking capture')

Delete the issue afterwards, and take the line out. An issue you made on purpose counts against the month like any other.

>telling servers apart

Every issue tells you how much of your fleet it is hitting, and that number is the one that says where to look. Three servers out of four is your script. Three out of four hundred is three operators with a broken config. The dashboard can only draw that distinction when servers arrive with names.

The name comes from a convar, and it is set by the person running the server — not by you. There is nothing to add to your script:

# server.cfg, on your customer's server
sets sv_projectName "Los Santos RP"

sets, not set. With set the value does not read back, so every event arrives unnamed and nothing says why.

Without it everything still works: errors are captured, grouped and alerted exactly the same. You just cannot tell one server from another, so an issue reads as servers not named instead of a share of your fleet. It is worth a line in your installation instructions.

The name is the only thing about a server that is recorded. There is no field in the payload for a player identifier, a licence or an IP address.

>knowing whether a fix worked

Every issue lists the releases it has been seen on, taken from the version line in your fxmanifest.lua. There is nothing to configure — the SDK reads it out of the manifest of whichever resource threw.

# fxmanifest.lua, in your resource
version '1.4.3'

Ship a fix and the issue says last seen on 1.4.2 once 1.4.3 is reporting other errors and not that one. It never says fixed: what is known is what arrived, and every operator still on the old release will keep hitting it until they update — which the panel tells you, because that is the part you can do something about.

The other direction is the one worth watching for. An issue that has only ever been seen on your newest release is marked new in 1.4.3 in the list — you shipped it. That is only claimed where it can be: an issue older than your retention window has had its early events pruned, so nothing is said rather than blaming a release that was not to blame.

Versions are ordered only when they can be: dotted numbers, with or without a v, with or without a -beta. A manifest versioned main is listed rather than ranked, because putting releases in an order nobody can prove is worse than not putting them in one.

>being told

Each script carries one Discord webhook, set on its settings page. Three things reach it: a fault nobody has seen before, one you marked fixed that came back, and one that suddenly starts happening a lot.

Only the third is rate limited. A new fault and a regression skip the cooldown on purpose — the cooldown exists to stop one broken loop repeating itself, not to hide a second, different bug that appears while the first is still quiet. A regression skips the spike threshold too: you shipped on the belief that it was fixed, one occurrence is enough to say otherwise, and waiting for fifty would mean hearing about it long after the release that caused it.

A spike is enough occurrences of one issue inside five minutes, and then at most one message every half hour for that issue. There is a send a test button beside the webhook field, because a URL pasted from the wrong Discord channel fails as silence and silence is what a correctly configured alert looks like most of the time.

>resolved, ignored, and back

Resolve means you fixed it. An occurrence afterwards is news: the issue reopens, the row is marked back, and Discord hears about it once.

Ignore means “I know, stop telling me”. It stays ignored however many arrive, and it beats every reason the alert rule can find — most often for something you cannot fix, like one operator’s broken config, which the fleet share on the issue points you at.

They are different claims and it matters which you use. Resolving something to quiet it is the one move that guarantees more noise, because the next occurrence counts as a regression and pings you.

>finding one

Issues never age out, so a script that has been selling for a year has more of them than one screen holds. The search box reads the message, the file and the resource name, with two prefixes for when you know which you mean.

nil value                    anywhere in the message, file or resource
resource:qb-banking          one resource of a bundle, exactly
file:inventory.lua           any file whose path contains that

Anything unrecognised falls through to the text, so a mistyped prefix narrows nothing rather than silently matching nothing. It is text and not a pattern — searching for 100% finds 100%. The status tab and the query survive each other, and both live in the URL, so a search is a link you can paste.

>if the DSN gets out

A DSN ships inside a script you sell, so everyone who bought it has a copy and someone who leaked the script leaked that too. What a stolen one does is write — junk issues in your dashboard, and your monthly allowance spent by somebody else.

Rotating issues a new key from the script’s settings. The old one stays valid for seven days by default, because your customers cannot update the moment you do and cutting them off means their errors stop arriving silently. Cutting off at once is the other option, for when the abuse is the thing you are stopping. The page shows whether the old key is still being used, so you can tell a finished rollout from one that will go dark on Friday.

>what does not
top-level chunk

Errors thrown while your resource is still loading, before init has run.

unwrapped natives

Errors inside native callbacks the SDK does not wrap.

engine crashes

Client crashes at the engine level. FiveM does not expose a usable stack for those, so we do not pretend to catch them.

>performance

Errors queue and flush on their own thread every five seconds, up to twenty at a time, and no more than thirty a minute per resource. If the API is unreachable the SDK drops the batch rather than blocking, retrying hard or writing to the console. A tracker that is noisier than the bug it reports gets uninstalled.

To check it works, throw something on purpose and watch it land:

CreateThread(function()
  local nothing = nil
  return nothing.field
end)

Running a server rather than selling a script? Live logs are the other half, they use a key of your own, and they need none of this.