Skip to content

Discord Telemetry

ScribeTelemetry sends profile and leaderboard health reports, errors and selected warnings to Discord. You can also enable performance alerts and regular summaries.

It is an optional add-on, not part of the Scribe package. Scribe sends nothing to Discord unless you install and start it.

Before you begin:

  1. Set up Scribe using Getting Started.
  2. Copy the ScribeTelemetry folder from addons/telemetry/ into ServerStorage, or insert ScribeTelemetry-Addon.rbxm from the release page there.
  3. Enable HTTP Requests in Experience Settings.
  4. Create a Discord webhook for the channel that should receive reports.

Telemetry has its own optional @rbxts/scribe-telemetry package; installing the core does not include it. See roblox-ts for publication status. After publication:

Terminal window
npm install @rbxts/scribe @rbxts/scribe-telemetry

Keep this configuration in a server script:

import Scribe from "@rbxts/scribe";
import ScribeTelemetry from "@rbxts/scribe-telemetry";
const telemetry = ScribeTelemetry.Start(Scribe, {
Webhooks: {
Alerts: { Url: "https://<proxy host>/api/webhooks/<id>/<token>" },
},
Default: "Alerts",
Routes: { Leaderboards: "Alerts" },
Leaderboards: { Enabled: true, Interval: 15, MaxBoards: 20 },
});
const monitor = telemetry.GetStats().Leaderboards;
if (monitor.State === "Available") {
print(monitor.Monitored, monitor.Omitted, monitor.SnapshotAge);
}

Destination names are inferred from Webhooks, so a misspelled route or telemetry.Test(...) destination is a type error. Options, preview kinds, and statistics are typed, including Leaderboards, SlowLoad, SlowLeavingHook, ReceiptCapacity, ReceiptRoutingRetry, LeaderboardDegraded, and LeaderboardRecovery previews. Calls that return success and a reason use a LuaTuple: const [queued, reason] = telemetry.Preview("Alerts", "LeaderboardDegraded").

Handle methods use ordinary TypeScript syntax such as telemetry.Stop(); roblox-ts emits the required Luau method call. SnapshotAge and LastError may be undefined. Monitor availability (Available, Unavailable, Disabled, DisabledInStudio, or Stopped) describes the monitor itself, rather than a particular board’s health. All routing, privacy, Studio, and delivery behavior below also applies to TypeScript.

-- ServerScriptService/EmberfallTelemetry.server.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local Scribe = require(ReplicatedStorage.Packages.Scribe)
local ScribeTelemetry = require(ServerStorage.ScribeTelemetry)
local telemetry = ScribeTelemetry.Start(Scribe, {
Webhooks = {
Alerts = { Url = "https://discord.com/api/webhooks/<id>/<token>" },
},
Default = "Alerts",
})

Put this script beside your Scribe server setup. Replace <id> and <token> with your webhook’s values. All enabled report categories go to Alerts.

By default, you receive profile and leaderboard health reports, errors, fatal errors and selected warnings. Performance alerts and regular summaries need extra configuration below.

The first example saves the handle returned by Start as telemetry. Use it for these separate tasks:

CallPurpose
telemetry:Test("Alerts")Queue one test message; returns (ok, reason).
telemetry:Preview("Alerts", "All")Queue synthetic examples of every report; returns (ok, reason).
telemetry:Flush(5)Wait up to five seconds for the queues to drain.
telemetry:GetStats()Read queue, delivery and destination state, without URLs.
telemetry:Stop()Stop monitoring and remove listeners. Calling it again is safe.

Start checks the configuration before connecting listeners. It errors for:

  • A URL that does not use http or https.
  • A route to an unknown destination, or an empty route list.
  • A role ID containing anything other than digits.
  • A performance rule with no threshold.

You cannot start a second handle for the same Scribe module until the first one has stopped.

Use Preview to see the report designs without causing an outage or changing player data. Keep the handle returned by Start, then call:

telemetry:Preview("Alerts") -- every scenario
telemetry:Preview("Alerts", "Health") -- one group
telemetry:Preview("Alerts", "Outage") -- one scenario

Every preview title starts with PREVIEW:, and each embed says its data is synthetic. Previews never mention a role or change the add-on’s view of the server. To try them in Studio, start the add-on with AllowStudio = true.

ResultMeaning
(true)All requested examples were queued. This does not confirm delivery.
(false, reason)A kind or destination was unknown, the destination was disabled or dead, a queue was full, or the add-on was stopped or disabled in Studio.

Previews use the same formatting and delivery queue as real reports. Your rate limits and configured Limits still apply. They appear in GetStats().Previews, separately from Reports.

KindScenarios
HealthHealthyStartup, Degraded, Outage, PartialRecovery, Recovery, IncidentUnderway
LeaderboardsLeaderboardDegraded, LeaderboardRecovery
IssuesError, Fatal, ReceiptCapacity
WarningsWarning, SlowLoad, SlowLeavingHook, ReceiptRoutingRetry
GroupingRepeat, UpstreamSuppressed
PerformanceSlowSaves, SaveFailures, Traffic, each with a Cleared counterpart
SummariesSummary, SummaryNoSamples, SummaryNoBudget
FormattingLongMessage, RedactedContext

The Scribe Studio plugin also offers previews under Diagnostics → Simulations. Choose a destination and a kind, then enable the plugin’s write toggle. The preview button is enabled when writes are on and a started add-on reports Running; otherwise, its tooltip explains what is missing.

What previews leave unchanged

Previews use the real report builders without changing summary baselines, performance streaks, repeat groups or mention cooldowns.

The add-on registers itself with Scribe.RegisterAddon when it starts, even when Studio has disabled it. That makes the plugin’s preview row visible, but AllowStudio = true is still required to send from Studio.

Give each webhook a name, then use Routes to choose which reports it receives. In this example, profile health, leaderboard health and summaries have their own channels, while performance alerts go to two channels.

Use this configuration in place of the one-webhook example; do not start both.

A category without a route uses Default. Setting its route to false disables it; it does not fall back to Default.

ScribeTelemetry.Start(Scribe, {
Webhooks = {
Alerts = {
Url = "https://discord.com/api/webhooks/<id>/<token>",
Mention = { RoleId = "123456789012345678" },
},
Health = { Url = "https://discord.com/api/webhooks/<id>/<token>" },
Boards = { Url = "https://discord.com/api/webhooks/<id>/<token>" },
Summaries = { Url = "https://discord.com/api/webhooks/<id>/<token>" },
},
Default = "Alerts",
Routes = {
Health = "Health",
Leaderboards = "Boards",
Summaries = "Summaries",
Performance = { "Alerts", "Health" },
Warnings = false,
},
Summaries = { Enabled = true, Interval = 1800 },
Performance = {
Rules = {
SlowSaves = { Threshold = 2 },
SaveFailures = { Threshold = 1 },
},
},
})

With Mention configured, Fatal and Outage reports can ping the role. By default, a destination can ping at most once every five minutes; routine reports do not ping. Use Mention.On to choose other report kinds and Mention.Cooldown to change the interval.

If two destination names use the same URL, each report is delivered there once.

CategoryWhat it reportsDefault
HealthProfile-service degradation, outages and recovery.On
LeaderboardsA board is degraded, or recovers.On
IssuesError and Fatal log entries.On
WarningsSelected Warn entries.On, selected codes only
PerformanceA configured threshold is exceeded, or the condition clears.Off until a rule has a threshold
SummariesRegular health, usage and performance reports.Off

Every report describes this server. An outage report says what this server observed; it is not a verdict on your whole game. Reports are not combined across servers, so ten servers in an outage can send ten reports.

Health listens to OnStatusChanged. Reports name the transition—degraded service, outage, partial recovery or full recovery—and how long the previous state was observed. If an incident was already underway when telemetry started, the report says so without guessing its start time.

Issues includes all errors and fatal errors except those listed in Issues.ExcludeCodes or Issues.ExcludeCategories.

Warnings includes a warning when its code is in Warnings.Include or its category is in Warnings.Categories. The default code list is available as ScribeTelemetry.DefaultWarningCodes. It focuses on warnings you may need to act on: large profiles, slow joins or leaving callbacks, held saves, purchase claims, unconfirmed gifts or passes, receipt retries, and leaderboard failures or budget delays.

The defaults include LB_READ_FAIL, LB_WRITE_FAIL, LB_QUEUE_OVERFLOW, LB_BUDGET_DEFERRED, PLAYER_LEAVING_HOOK_SLOW and RECEIPT_RETRY. RECEIPT_HISTORY_FULL is an error, so it uses Issues without a warning opt-in.

The add-on checks Scribe.GetLeaderboardSnapshot() every 30 seconds by default. This reads cached status from all active bundles using that Scribe module; it makes no DataStore requests. Each board is identified by its numeric BundleId and name, so two bundles can both have a Coins board.

A board already degraded at the first check sends an alert. Later changes to Degraded or back to Healthy send another report. Starting is quiet. A board that disappears, for example when its bundle stops, is forgotten without a recovery claim. Detection follows the polling interval.

OptionDefaultPurpose
Leaderboards.EnabledtrueEnable board monitoring.
Leaderboards.Interval30Seconds between checks; minimum 5.
Leaderboards.MaxBoards64Bound the number of boards tracked.

Routes.Leaderboards follows the same Default fallback as other categories; set it to false to stop transition alerts while keeping monitoring and summaries. Leaderboards.Enabled = false stops snapshot polling and marks the summary disabled. When there are more than MaxBoards, the first boards by bundle ID and name are tracked; summaries show how many were monitored and omitted. Omitted boards are not monitored. Board health is separate from profile health: a healthy profile service does not mean every leaderboard is updating.

Older Scribe versions without the snapshot API still work. Board reports are unavailable, and summaries say so rather than assuming the boards are healthy.

A performance rule must hold for Performance.Checks consecutive checks, taken Performance.Interval seconds apart. It sends another report when the condition clears. Each enabled rule needs a Threshold.

RuleThreshold measures
SlowSavesp99 (99th percentile) save duration, in seconds. Each check needs MinSamples fresh samples.
SaveFailuresSave failures per minute.
TrafficBytesOut per second.

Summaries run every Summaries.Interval seconds. Profile health labels the profile-service status separately from the leaderboard section. Reports include:

  • Sessions, load/save counts and outgoing bytes per second.
  • Leaderboard read/write activity, failures, skipped writes, queue overflows, budget deferrals and the last reported queue depth. That queue gauge comes from the last reporting bundle; it is not a server-wide total.
  • A bounded list of monitored boards, including pending writes and cache age. Disabled or unavailable monitoring and omitted boards are identified.
  • Receipt-history refusals and dropped or failed log-sink deliveries.
  • Save, profile-load, total join and leaving-callback duration percentiles, plus profile size. Each distribution includes its sample window.

Use per-board pending counts for each board’s backlog. The DataStore budget is included when the server can read it. An empty sample window is reported as such; it is not a zero-duration measurement. Older Scribe versions may omit measurements they do not provide.

The first occurrence of a code is reported immediately. Further occurrences are grouped into a follow-up after Grouping.Interval (60 seconds by default). When an entry identifies a player or key, each subject has its own group.

The count is how many entries telemetry saw. Scribe may already have suppressed some repeated logs; their Repeats context value is reported separately. In that case, the follow-up says the underlying total is higher instead of claiming an exact count.

Each message uses Scribe’s name and logo. The branding is fixed; Start rejects IconUrl and BrandName options.

The footer includes a report ID, place version, your configured Environment and the time in UTC. Discord also displays the time in your local zone. The report ID combines the first eight characters of the server ID with a counter.

Live-server messages include a Server link that joins that server, provided it has a place ID and JobId. Studio messages omit the join link. Health reports and summaries also include the place ID and Scribe version.

Player identifiers are redacted by default. Set IncludePlayerIds = true only if you want them included in reports sent to your endpoint.

With the default setting:

  • Non-table values under player context fields become [player]. Recognized names include UserId, RecipientId, BuyerId, Player, and names ending in UserId or PlayerId. The same rules apply inside retained nested tables.
  • UserId=..., Player=..., and Player instances in log context are also redacted as [player].
  • Non-table values under context fields named Key or ending in Key, plus grouping subjects built from them, become [key]. Profile keys can contain player identifiers even without a recognizable prefix.
  • Keys in message text lose their digits when they use the configured ProfileKeyPrefix or another prefix ending in an underscore. The add-on asks Scribe for its current prefix on every entry.

Context tables are limited to twelve keys and two levels. Functions, threads and unsupported Roblox values are dropped. Non-player instances are reduced to their class and name. Text is shortened at a valid UTF-8 boundary to fit Discord’s limits. Strings over 4 KiB (4096 bytes) are replaced with [oversized text omitted] before redaction. This bounds processing work and avoids exposing part of a secret by cutting it before redaction.

Delivery is best effort: reports can be dropped. HTTP requests run in a delivery worker, so a slow endpoint does not block Scribe. Each log sink also runs in its own thread. One failing webhook does not hold up another.

Queues have these default limits:

OptionDefault
Limits.PerDestination64 reports per destination
Limits.MaxQueued200 reports overall
Limits.MaxQueuedBytes256 KB overall
Limits.MaxAge15 minutes
Limits.MaxAttempts5 delivery attempts

When a queue is full, it drops the oldest expired report first. Otherwise, it drops the lowest-priority report whose priority is no higher than the incoming one. A summary can make room for an issue; an issue is never dropped to make room for a summary. Reports older than Limits.MaxAge are dropped.

ResponseWhat the add-on does
Any 2xxCounts delivery as successful.
429, or X-RateLimit-Remaining: 0Respects the destination’s rate limit.
5xx or a network errorRetries with increasing delays, up to Limits.MaxAttempts.
401, 403, 404 or 410Marks the destination dead and drops its queue.
400, 413 or 422Drops the invalid report without retrying it.

A timed-out request is counted as Uncertain: it may have reached the endpoint even though the response did not arrive. Use GetStats() to inspect delivery and per-destination state without exposing the configured URLs.

You can send reports to your own service instead of a Discord webhook. Url accepts an http or https URL with a host name or IP address, such as https://api.example.com/telemetry/alerts.

The service receives Discord’s JSON format as application/json: username, avatar_url, embeds, allowed_mentions, and content when a mention is included. Return any 2xx status to acknowledge delivery. The response and retry rules above apply to every host.

The add-on adds wait=true only to URLs with Discord’s /api/webhooks/ path. Every configured URL is redacted from outgoing messages, including custom URLs.

The add-on uses --!strict and exports Options, Handle, Stats and ScribeModule. With Luau’s new type solver, the editor can flag misspelt options, wrong value types, unknown mention kinds and invalid stat access before you run the script.

Implementation and type checks

The add-on uses only Scribe’s public diagnostics API: one log sink, one status connection, metric readers and cached leaderboard snapshots. A game script can access the same signals.

test/TypeCheck.luau checks the exported types, and test/TypeCheckErrors.luau checks that invalid shapes are rejected.

  • Diagnostics is what these reports are made of: the log sink, the health machine and the metric readers.
  • Log Code Reference lists every code an Issues or Warnings report can name.
  • Scribe Studio shows the same signals live in a dock while you play-test.