Custom Exporters & Middleware
Realistic Analytics lets you plug in your own exporters or insert middleware functions into the event pipeline before events are buffered.
flowchart LR
A[Telemetry Event] --> B[Middleware Pipeline]
B -->|Transformed Event| C[BufferQueue]
B -->|nil / Filtered| X[Dropped]
C --> D[RobloxAnalytics]
C --> E[DebugConsole]
C --> F[HttpExporter]
C --> G[Custom Exporter]
1. Custom Middleware
Section titled “1. Custom Middleware”Middleware runs synchronously whenever an event is dispatched. You can add extra metadata to events or drop specific events by returning nil.
Registering Middleware
Section titled “Registering Middleware”Call Analytics:Use(fn) on the server instance:
Analytics:Use(function(event: Types.TelemetryEvent): Types.TelemetryEvent? -- Return modified event, or nil to discard return eventend)Example: Attaching Game Version
Section titled “Example: Attaching Game Version”local RunService = game:GetService("RunService")
Analytics:Use(function(event) event.data.environment = if RunService:IsStudio() then "Studio" else "Production" event.data.placeVersion = game.PlaceVersion return eventend)Example: Filtering Out High-Frequency Events
Section titled “Example: Filtering Out High-Frequency Events”Analytics:Use(function(event) -- Ignore heartbeat snapshots during local Studio playtesting if event.name == "ServerHeartbeat" and RunService:IsStudio() then return nil end return eventend)2. Writing a Custom Exporter
Section titled “2. Writing a Custom Exporter”An exporter is any table implementing the IExporter interface:
export type IExporter = { name: string, exportBatch: (events: { Types.TelemetryEvent }) -> (boolean, string?), flush: () -> (),}name: An identifier used for logging and debugging.exportBatch(events): Receives a table of events. Return(true, nil)on success, or(false, errorMessage)on failure.flush(): Called when the server shuts down insidegame:BindToClose.
3. Real-World Recipe: Discord Webhook Alert Exporter
Section titled “3. Real-World Recipe: Discord Webhook Alert Exporter”Here is a practical custom exporter that sends batched event alerts to a Discord channel via webhooks, with rate-limiting and secret management:
local HttpService = game:GetService("HttpService")
-- Fetch secret safely via Roblox Secretslocal webhookSecret: any = nillocal secretOk, secretVal = pcall(function() return HttpService:GetSecret("webhook")end)if secretOk and secretVal then webhookSecret = secretValend
local DiscordExporter = {}DiscordExporter.__index = DiscordExporter
function DiscordExporter.new(targetSecret: any) local self = setmetatable({}, DiscordExporter) self.name = "DiscordWebhookExporter" self._secret = targetSecret self._queue = {} :: { any } self._isProcessing = false self._lastDispatch = 0 return selfend
function DiscordExporter:exportBatch(events: { any }): (boolean, string?) if #events == 0 or not self._secret then return true, nil end
for _, ev in ipairs(events) do table.insert(self._queue, ev) end
self:_drainQueue() return true, nilend
function DiscordExporter:_drainQueue() if self._isProcessing then return end self._isProcessing = true
task.spawn(function() local DISPATCH_INTERVAL = 3.0 -- Keep well within Discord's rate limits local MAX_PER_MESSAGE = 10
while #self._queue > 0 do local elapsed = os.clock() - self._lastDispatch if elapsed < DISPATCH_INTERVAL then task.wait(DISPATCH_INTERVAL - elapsed) end
local batch = {} local count = math.min(#self._queue, MAX_PER_MESSAGE) for _ = 1, count do table.insert(batch, table.remove(self._queue, 1)) end
local lines = {} for _, ev in ipairs(batch) do table.insert(lines, string.format("• **%s** (`%s`)", ev.name, ev.category)) end
local payload = HttpService:JSONEncode({ username = "Game Telemetry", embeds = { { title = string.format("Telemetry Batch (%d Events)", #batch), description = table.concat(lines, "\n"), color = 0x00A2FF, footer = { text = "Job: " .. (if game.JobId ~= "" then game.JobId else "Studio") }, timestamp = DateTime.now():ToIsoDate(), } } })
local ok, res = pcall(function() return HttpService:RequestAsync({ Url = self._secret, Method = "POST", Headers = { ["Content-Type"] = "application/json" }, Body = payload, }) end)
self._lastDispatch = os.clock()
if not (ok and res.Success) then local code = if ok then res.StatusCode else 0 if code == 429 then task.wait(5) -- Pause if throttled by Discord end end end
self._isProcessing = false end)end
function DiscordExporter:flush() -- Queue is drained by background workerend
return DiscordExporterRegistering the Exporter
Section titled “Registering the Exporter”if webhookSecret then Analytics:AddExporter(DiscordExporter.new(webhookSecret)) print("Discord telemetry exporter registered!")end