Skip to content

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]

Middleware runs synchronously whenever an event is dispatched. You can add extra metadata to events or drop specific events by returning nil.

Call Analytics:Use(fn) on the server instance:

Analytics:Use(function(event: Types.TelemetryEvent): Types.TelemetryEvent?
-- Return modified event, or nil to discard
return event
end)
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 event
end)

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 event
end)

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 inside game: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 Secrets
local webhookSecret: any = nil
local secretOk, secretVal = pcall(function()
return HttpService:GetSecret("webhook")
end)
if secretOk and secretVal then
webhookSecret = secretVal
end
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 self
end
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, nil
end
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 worker
end
return DiscordExporter
if webhookSecret then
Analytics:AddExporter(DiscordExporter.new(webhookSecret))
print("Discord telemetry exporter registered!")
end