{"id":"spiraljng/sentryroblox","name":"sentryroblox","scope":"spiraljng","platform":"roblox","description":"An unofficial Sentry SDK for Roblox","version":"2.2.0","latest":"2.2.0","versions":["2.0.0","2.1.0","2.1.3","2.2.0"],"license":"BSD-2-Clause","licenseRating":"safe","licenseCaveats":["The copyright notice must be preserved in copies."],"licenseVerified":true,"dependencies":{},"integrity":"d4bbd23688ed391dab005717730abfae966de8a2bdcac3979d982648d4eda07c","likes":0,"downloads":0,"install":"forest install spiraljng/sentryroblox","url":"https://forest.dev/p/roblox/spiraljng/sentryroblox","files":"https://api.forest.dev/ai/package/roblox/spiraljng/sentryroblox/files","readme":"# SentryRoblox\n\nUnofficial [Sentry](https://sentry.io) SDK for Roblox/Luau.\n\nWorks with any Sentry-compatible error tracking software, including open-source Sentry alternatives like [GlitchTip](https://glitchtip.com/) and [Bugsink](https://www.bugsink.com/).\n\n### Features\n\n- Automatic error and warning captures\n- A curated noise blocklist that updates without an SDK release\n- Scrubs PII and generalizes message info to preserve issue grouping\n- Built in client relay without DSN leakage\n- Fully typed\n- Tested (programmatically and in live-service games)\n\n### Installation\n\n\nFirst, map your packages folder in your Rojo project file:\n\n```json\n\"ReplicatedStorage\": {\n\t\"$className\": \"ReplicatedStorage\",\n\t\"Packages\": {\n\t\t\"$className\": \"Folder\",\n\t\t\"$path\": \"roblox_packages\" // \"Packages\" for wally\n\t}\n}\n```\n\n<details>\n\n<summary>using pesde</summary>\n\n```sh\npesde add spiraljng/sentryroblox --target roblox --alias SentryRoblox\n```\n\n\nRequire it as `require(game.ReplicatedStorage.Packages.SentryRoblox)`.\n\n</details>\n\n<details>\n\n<summary>using wally</summary>\n\nin `wally.toml`:\n\n```toml\n[dependencies]\nSentryRoblox = \"spiraljng/sentryroblox@2.1.0\"\n```\n\n```sh\nwally install\n```\n\nRequire it as `require(game.ReplicatedStorage.Packages.SentryRoblox)`.\n\n</details>\n\n<details>\n\n<summary>using the model file</summary>\n\nDownload `SentryRoblox.rbxm` from the [releases page](https://github.com/spiraljng/sentryroblox/releases) and drop it into `ReplicatedStorage`.\n\nRequire it as `require(game.ReplicatedStorage.SentryRoblox)`.\n\n</details>\n\n### Usage\n\n#### Initialising\n\n```lua\nlocal SentryRoblox = require(game.ReplicatedStorage.SentryRoblox)\n\nSentryRoblox:Init({\n\tDSN = \"https://example@o0.ingest.sentry.io/0\",\n})\n```\n\n`DSN` is the only argument you have to pass. `Init` does nothing without one\n\n**`Init` is server-only.** See [the client relay](#the-client-relay) for more info regarding client captures.\n\n| option           | type      | default                      |\n| ---------------- | --------- | ---------------------------- |\n| `DSN`            | `string`  | required                     |\n| `Release`        | `string`  | `PlaceId@PlaceVersion`       |\n| `Environment`    | `string`  | `\"studio\"` or `\"production\"` |\n| `SampleRate`     | `number`  | `1.0`                        |\n| `SendDefaultPII` | `boolean` | `false`                      |\n| `Debug`          | `boolean` | `false`                      |\n\n**`Release`** defaults to a string built from the place id and place version. Override it with your own versioning if available\n\n**`Environment`** is derived from `RunService:IsStudio()`, so Studio traffic and live traffic do not mix in your issue list. If you override this flag, it is recommended you keep studio traffic separate\n\n**`SampleRate`** samples error events (only sends the specified % of requests). Sentry's advice is to sample transactions rather than errors, so leave this alone unless you have some reason for it\n\n**`SendDefaultPII`** controls how much of the player's info gets attached. See [Privacy and grouping](#privacy-and-grouping)\n\n**`Debug`** prints extra detail when an integration fails to set up or a capture throws. Off by default.\n\nDelivery failures are the exception. The first failed request always prints, with the status and the reason, so a misconfigured SDK never fails silently. It stays quiet until a request succeeds again.\n\n#### Capturing errors\n\nBy default, you do not have to do anything. Once `Init` has run, these are captured automatically:\n\n- Uncaught errors, via `ScriptContext.Error`\n- Warnings, via `LogService.MessageOut`\n- Errors and warnings on the client are [forwarded over the relay](#the-client-relay)\n\nFor everything else:\n\n```lua\nSentryRoblox:CaptureMessage(\"checkpoint reached\", \"info\")\n\nSentryRoblox:CaptureException(\"inventory failed to reconcile\")\n\nSentryRoblox:CaptureEvent(Event, Hint?)\n```\n\n`CaptureMessage` takes a level: `fatal`, `error`, `warning`, `info` or `debug`\n\nTo catch an error rather than report one you already have, use `ExceptionHandler`. It returns a function that works as `xpcall`'s message handler:\n\n```lua\nxpcall(Reconcile, SentryRoblox:ExceptionHandler())\n```\n\nErrors thrown inside a Sentry handler cannot recurse back through the SDK. A failure while reporting a failure is dropped rather than looping\n\n#### Scopes\n\nA scope is a collection of context that gets copied onto an event at the moment it is captured.\n\nThere are two levels worth thinking about. A top-level scope that describes the server, and lower-level scopes for narrow operations (examples below)\n\n##### Top-level scope\n\nThe main hub's scope is active from `Init` onwards, for the whole session, until you change it. Context that is true for the entire server belong here.\n\n```lua\nSentryRoblox:ConfigureScope(function(Scope)\n\tScope:SetTag(\"region\", \"eu-west\")\n\tScope:SetTag(\"server_type\", \"ranked\")\n\tScope:SetExtra(\"matchmaker_version\", \"3.1.0\")\nend)\n```\n\nSet it once at startup and leave it. If you find yourself wanting to change one of these per player, that is the sign it belongs in a lower scope instead\n\n##### Lower-level scopes\n\nFor context that is only true of one piece of work, one player, or one situation. `WithScope` pushes a copy of the active scope, runs your callback, then throws the copy away\n\n```lua\nSentryRoblox:WithScope(function(Scope)\n\tScope:SetTag(\"phase\", \"matchmaking\")\n\tScope:SetExtra(\"queue_size\", 12)\n\n\tRunMatchmaking()\nend)\n```\n\nIt starts as a copy, so it inherits the top-level tags and then diverges. `region=eu-west` is still on the event, plus `phase=matchmaking`, and the phase is gone once the callback returns\n\nThat is what makes per-player tagging work:\n\n```lua\nlocal function RunInBiome(Player: Player, BiomeName: string)\n\tSentryRoblox:WithScope(function(Scope)\n\t\tScope:SetTag(\"biome\", BiomeName)\n\t\tScope:SetUser(Player)\n\n\t\txpcall(function()\n\t\t\tDoBiomeWork(Player, BiomeName)\n\t\tend, SentryRoblox:ExceptionHandler())\n\tend)\nend\n```\n\nPlayer A in Forest reports `region=eu-west, biome=Forest`. Player B in Volcano reports `region=eu-west, biome=Volcano`\n\nThe catch is timing. The scope is read when the event is captured, so a tag only lands on errors captured while the callback is still running. An error from a `task.delay` or a later remote event has no biome tag, which is why the example catches its own error rather than relying on automatic capture. It also means `WithScope` is for synchronous blocks\n\nTwo variations on the same idea:\n\n|                              |                                                                                          |\n| ---------------------------- | ---------------------------------------------------------------------------------------- |\n| `PushScope()` / `PopScope()` | `WithScope` by hand, when you need to restore somewhere other than the end of a callback |\n| `Clone()`                    | A whole separate hub with its own top-level scope                                        |\n\n`Clone` is for context tied to one thing for a long time. For example, `TrackSessions` uses it to give each player their own hub and so session state never bleeds between players. If you find yourself setting and unsetting the same tag around every call, that is a sign you want a cloned hub instead.\n\n##### What goes in a scope\n\n| method                                        | use for                                                        |\n| --------------------------------------------- | -------------------------------------------------------------- |\n| `SetUser(Player \\| number \\| nil, Identity?)` | Who the event belongs to, see [Privacy](#privacy-and-grouping) |\n| `SetTag` / `SetTags`                          | Short strings you want to filter by                            |\n| `SetExtra` / `SetExtras`                      | Arbitrary data you only want to read                           |\n| `SetContext(Key, Table)`                      | A named group of related fields                                |\n| `SetLevel(Level)`                             | Default level for events from this scope                       |\n| `SetTransaction(Name)`                        | Names the current transaction                                  |\n| `SetFingerprint({ \"name\" })`                  | Forces grouping, see [Privacy](#privacy-and-grouping)          |\n| `AddEventProcessor(P)`                        | Scope-local processor                                          |\n| `Clear()` / `Clone()`                         | Reset or deep copy                                             |\n\n**Tags** are indexed, so Sentry can filter and search by them. Use them for things with a small set of values: biome, game mode, difficulty, server region. **Extras** and **contexts** are only readable. Use them for detail you want to see on an issue but would never filter by, like an inventory table or a stat block. Neither affects grouping\n\n**`SetUser`** takes a `Player`, a raw `UserId`, or `nil` to clear it. One at a time. What actually gets attached depends on `SendDefaultPII`.\n\n**`SetFingerprint`** is the bandaid fix for when an error splits into more issues than it should. See [Privacy](#privacy-and-grouping) for why that happens and what else you can do about it\n\n**`Clear()` replaces the scope with a fresh one, but keeps `release`, `environment`, `server_name` and `dist`.** Those come from `Init` rather than from you, so clearing in the middle of a session will not drop them from later events. Everything else, including tags, extra, contexts, breadcrumbs and the user, is reset. If you only want to drop accumulated context, clear the parts you mean rather than the whole scope\n\n**`Clone()` deep copies.** Mutating the clone never touches the parent, including nested tables like `tags` and `breadcrumbs`\n\n##### Breadcrumbs\n\nA breadcrumb is a note about something that happened, kept in a rolling buffer and attached to the next error\n\nNothing is recorded automatically, so you add them at the points that matter:\n\n```lua\nSentryRoblox:ConfigureScope(function(Scope)\n\tScope:AddBreadcrumb({ message = \"opened the vault\", level = \"info\" })\n\tScope:AddBreadcrumb({\n\t\tcategory = \"combat\",\n\t\tmessage = \"hit by lava\",\n\t\tlevel = \"warning\",\n\t\tdata = { damage = 40 },\n\t})\nend)\n```\n\nBreadcrumbs live on the scope you write them to, and an event only carries the breadcrumbs from the one scope that was active when it was captured. That matters as soon as you have more than one scope. A breadcrumb added inside `WithScope` disappears with the block, and one added to a cloned hub belongs to that hub alone\n\nPut them on the scope whose events you want them on. Uncaught errors are captured on the main hub, so a breadcrumb written to a per-player scope will not reach them, and Roblox never says which player an uncaught server error belongs to, so there is nothing to look up\n\nThe buffer is capped by `MaxBreadcrumbs` and drops the oldest first, so you can log freely without thinking about payload size. They are attached oldest first, which puts the newest one nearest the error\n\n`ClearBreadcrumbs()` empties the buffer. `BeforeBreadcrumb` filters them globally, which is useful when you want to keep a whole category out of every event without editing each call site\n\n##### Advanced configurations\n\n<details>\n\n<summary>Options</summary>\n\n| option                 | type            | default                   |\n| ---------------------- | --------------- | ------------------------- |\n| `ServerName`           | `string`        | `game.JobId` or `\"local\"` |\n| `MaxBreadcrumbs`       | `number`        | `100`                     |\n| `AttachStacktrace`     | `boolean`       | `false`                   |\n| `SendStudioEvents`     | `boolean`       | `false`                   |\n| `SendClientEvents`     | `boolean`       | `true`                    |\n| `InAppInclude`         | `{string}`      | `{}`                      |\n| `InAppExclude`         | `{string}`      | `{}`                      |\n| `BeforeSend`           | `function`      | none                      |\n| `BeforeBreadcrumb`     | `function`      | none                      |\n| `DefaultIntegrations`  | `boolean`       | `true`                    |\n| `Integrations`         | `{Integration}` | `{}`                      |\n| `DisabledIntegrations` | `{string}`      | `{}`                      |\n| `Transport`            | `Transport`     | built-in HTTP             |\n\n**`AttachStacktrace`** attaches a stack trace to messages as well as exceptions. Two consequences before you turn it on:\n\n1. Sentry groups events with traces separately from events without, so it reshuffles your issue list\n2. `LogService` warnings take their trace from the SDK's handler rather than from the code that logged them, so they all share one trace and collapse into a single issue\n\nIt is useful for `CaptureMessage` calls you make yourself, not for warnings\n\n**`SendStudioEvents`** is off by default, which means **nothing you do in Studio is reported**. The reason for the default is that warnings fire constantly in Studio and most are not worth sending.\n\nWhen events are being dropped for this reason, the SDK prints a one-time notice in the output.\n\n**`InAppInclude`** and **`InAppExclude`** control which stack frames Sentry treats as yours. Both are lists of plain substrings, matched against frame paths.\n\nFrames are in app by default, except Roblox's internals, which are dropped entirely. `InAppExclude` is a veto and is checked first, so a path matching both lists is excluded.\n\n**`InAppInclude` is a whitelist.** Set it and only frames matching one of its entries count as yours, and everything else is marked as a library. Leave it empty and everything not excluded stays yours. This is stricter than Sentry's own SDKs, where the include list only promotes frames and a project-root heuristic supplies the default\n\nSentry uses in-app frames to decide which part of a trace to blame, so this is how you stop third-party code from becoming the headline of an issue. Frames from the SDK itself are always excluded.\n\n```lua\nSentryRoblox:Init({\n\tDSN = DSN,\n\tInAppInclude = { \"ServerScriptService\", \"ReplicatedStorage.MyGame\" },\n})\n```\n\nThat says only your own `ServerScriptService` and `MyGame` code is yours, and everything else, including any library you vendored into `ReplicatedStorage`, is treated as third party. Reach for `InAppExclude` instead when it is easier to list what is not yours\n\n**`BeforeSend`** runs after the scope merge and before encoding. Return the event to send it, return `nil` to drop it. This SDK already drops Roblox's internal noise plus the noise blocklist below, but you may need to make some alterations\n\n```lua\nSentryRoblox:Init({\n\tDSN = DSN,\n\tBeforeSend = function(Event)\n\t\tif Event.message and string.find(Event.message.message, \"known noise\") then\n\t\t\treturn nil\n\t\tend\n\n\t\treturn Event\n\tend,\n})\n```\n\n**`BeforeBreadcrumb`** does the same for breadcrumbs.\n\n**The noise blocklist** is a curated list of unavoidable Roblox engine noise, kept in [`noise/blocklist.json`](noise/blocklist.json) and fetched at runtime, so a rule can be added or retired without an SDK release. Rules match the raw message and traceback, before scrubbing, and apply to uncaught errors, `LogService` warnings and relayed client events. The list is pushed to clients over the relay as well, so known noise never reaches the server or spends a player's relay budget\n\nA snapshot of the list ships inside the SDK. It is what filters when HTTP requests are disabled, and in the moment before the first fetch lands, so a server without HTTP still keeps the rules that existed when the version was released\n\n| option            | type     | default                     |\n| ----------------- | -------- | --------------------------- |\n| `URL`             | `string` | the `main` copy of the list |\n| `RefreshInterval` | `number` | `3600`                      |\n| `ExtraRules`      | `{Rule}` | `{}`                        |\n\nA rule is `{ Id, Match, Type }`, where `Type` is `\"substring\"` or","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/spiraljng/roblox/sentryroblox/2.2.0/readme"}