ErrorWatch
Captures script errors and scheduler task crashes, and reports each unique error once per window with an occurrence count.
- Realm
- Shared
- Require
ReplicatedStorage.ChloeKernel .ErrorWatch - Since
- v0.1.0
- Source
- ErrorWatch.luau
ErrorWatch listens to every script error on its machine, groups identical ones, and reports each unique error once per
window with a running count. A task that crashes 4,000 times in 30 seconds becomes one log line ending in (x4000)
instead of 4,000 lines. It attaches the same way on a server or a client kernel, and nothing in the framework attaches
it for you. GcWatch is the sibling for memory health; reports go out through
Logger and the Bus.
Mental model
Section titled “Mental model”ErrorWatch merges two streams into one report:
ScriptContext.Error: every unhandled script error on this machine, kernel code or not.kernel.Scheduler.OnError: task crashes the Scheduler caught. The scheduler runs tasks protected, so these never reachScriptContext, and a crash-looping scheduled task would be invisible without this second connection. The error’s first line becomes the message, the full text becomes the trace, and the source readsKernelScheduler.
Both streams go through the same grouping:
- The key is the message plus the first line of the traceback. The same message raised from two call sites counts as two errors; the same line failing 4,000 times counts as one.
- The first report of a key logs at once through
Logger.scope("ErrorWatch")at error level, formatted{message} [{source}] (x{count}), and publishesKernel.ScriptErrorwhen a kernel was given. Repeats insideWindowSeconds(default 30) only increment the count. The first repeat after the window logs and publishes again with the accumulated count. - Messages that interpolate names, ids or positions create a new key each. To keep the table from growing for the whole
server lifetime, a report prunes entries unseen for
WindowSeconds * 10(300 s by default), at most once per window. Pruning runs inside a report, so a quiet server keeps its table until the next error arrives.
Sinks and Bus subscribers see at most one line per unique error per window, and the count keeps every occurrence.
--!strict-- ErrorService: groups script errors and flags crash loops.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ErrorWatch = require(ReplicatedStorage.ChloeKernel.ErrorWatch)
local CrashLoopCount = 1000
local ErrorService = { Name = "ErrorService",}
function ErrorService.init(self: any, kernel: any) self.Kernel = kernelend
function ErrorService.start(self: any) local Kernel = self.Kernel self.Watcher = ErrorWatch.attach(Kernel) -- Runs once per unique error per window; count is every occurrence so far self.Connection = Kernel.Bus:subscribe("Kernel.ScriptError", function(_topic, message, _trace, source, count) if count >= CrashLoopCount then Kernel.Bus:publish("Ops.CrashLoop", source, message, count) end end)end
function ErrorService.stop(self: any) if self.Connection then self.Connection:disconnect() end if self.Watcher then self.Watcher:destroy() endend
return ErrorServiceAnalytics with WatchErrors = true turns each report into a ScriptError event for a custom
Destination. To alert on every error-level log line, add a Logger sink that filters
Logger.Level.Error. ErrorWatch’s reports reach that sink once per unique error per window, so a crash loop cannot
flood a webhook.
API reference
Section titled “API reference”| Member | Description |
|---|---|
ErrorWatch.attach(kernel?, options?) -> ErrorWatch |
Connects ScriptContext.Error (unless SkipBind) and, when the kernel has one, kernel.Scheduler.OnError. Without a kernel, reports still log but nothing publishes. |
watcher:counts() -> { [string]: number } |
A copy of the occurrence counts, keyed by message .. "\n" .. firstTraceLine. |
watcher:destroy() |
Disconnects both streams and clears the table. |
watcher:_report(message, trace, sourceName, now?) -> boolean |
Feeds one error through the grouping. Returns true when it logged. The spec seam for SkipBind. |
Options, all optional:
| Field | Default | Description |
|---|---|---|
WindowSeconds |
30 |
Reporting window per key. Entries unseen for ten windows are pruned. |
SkipBind |
false |
Do not connect ScriptContext.Error. Specs feed _report directly. |
Bus topics
Section titled “Bus topics”| Topic | Payload | Fired |
|---|---|---|
Kernel.ScriptError |
(message, trace, sourceName, count) |
On the first occurrence of a key, then at most once per window with the accumulated count. sourceName is the failing script’s GetFullName(), "?" when unknown, or "KernelScheduler" for a scheduler task crash. Only when attached with a kernel. |
ErrorWatch also consumes Scheduler.OnError, a Signal on the scheduler rather than a Bus topic.
Design notes
Section titled “Design notes”- ErrorWatch requires Logger relatively, so a cloned kernel logs through its own Logger and sinks instead of the original’s.
ErrorWatch.spec.luaudrives_reportwithSkipBindand asserts the first log and publish, silent counting inside the window, the accumulated count after it, and that different errors never suppress each other.