@avasapp/rozenite-plugin-ably
v1.2.0
Published
Ably Realtime inspector for React Native DevTools — inspect channels, stream live events, and preview decoded payloads.
Readme
@avasapp/rozenite-plugin-ably
An Ably Realtime inspector for React Native DevTools, built on Rozenite.
The Network Activity panel already shows you websocket frames. This shows you Ably: which channels are attached right now, who is subscribed to them, what messages actually arrived, and what was inside them.

Install
npm install --save-dev @avasapp/rozenite-plugin-ablyRequires Rozenite 2.2 or later. Rozenite discovers the plugin
automatically — no metro.config change is needed beyond having Rozenite itself
set up.
The plugin ships no runtime dependencies of its own. @rozenite/plugin-bridge,
@rozenite/agent-bridge and @rozenite/agent-shared are peer dependencies, so
the plugin shares your app's copy of the bridge rather than resolving a second one
alongside it — two bridge instances mean the panel never connects. Any Rozenite
2.x works; only a Rozenite major needs a matching release here.
Usage
Call the hook once, anywhere in your component tree, with your Ably.Realtime
client:
import { useAblyDevTools } from '@avasapp/rozenite-plugin-ably'
function DevTools() {
useAblyDevTools(ablyRealtimeClient)
return null
}It is a no-op outside __DEV__, and a no-op while the client is null, so it is
safe to call before the client exists:
useAblyDevTools(isLoggedIn ? client : null)Channel labels
Ably channel names are often opaque (device_7b41-…). If your app knows which
feature subscribed to what, feed that in and the panel will show it on each
channel:
useAblyDevTools(client, {
labels: {
getLabels: () => ({ 'device_7b41': ['telemetry-screen', 'chat'] }),
subscribe: (onChange) => registry.onChange(onChange), // optional
},
})useAblyDevTools(getAblyJsClient(), {
labels: {
getLabels: () =>
Object.fromEntries(
getTransport()
.getChannels()
.map((c) => [
c.name,
c.listenerDetails
.map((l) => l.label)
.filter((l): l is string => Boolean(l)),
]),
),
subscribe: (onChange) => getTransport().onChannelsChange(onChange),
},
})Options
| Option | Default | Description |
| ----------------- | ------- | ----------------------------------------------------------------------------- |
| labels | — | Maps channel names to human-readable labels. |
| maxEvents | 1000 | Ring-buffer size. Older events are dropped once exceeded. |
| captureProtocol | false | Capture raw ably-js protocol frames. Verbose; also toggleable from the panel. |
| enabled | true | Escape hatch. The plugin is already inert outside __DEV__. |
What you get
Channels — every channel the client has touched, with its live attach state, subscriber count, per-channel in/out counters, error reason, and time in state. Detached and released channels are retained behind a toggle, because "the channel I expected isn't there" is the bug you most often need to see.
Events — a filterable stream of messages, presence, channel/connection state transitions, and errors. Filter by kind, by channel, or by free text that searches payload contents too.
Payloads — Ably delivers most payloads as a JSON string. The plugin parses it and keeps the original, so you get a real collapsible tree by default and the exact bytes on demand. Searching expands the tree far enough to reveal matches rather than filtering them out of their surrounding structure.
Agent tools
The same session is exposed to Rozenite for
Agents as the avasapp/ably domain, so a coding
agent can inspect realtime traffic from the terminal with no DevTools window
open:
npx rozenite agent session create
npx rozenite agent avasapp/ably call --tool list-channels \
--args '{"onlyErrored":true}' --session <id>| Tool | |
| --- | --- |
| get-connection | State, failure reason, retryIn, capabilities. |
| list-channels | Paginated. Filters: state, search, onlyErrored, includeReleased. |
| read-channel | One channel in full, including its listeners. |
| list-events | Paginated. Filters: channel, kind, dir, search, since, order. |
| read-event | One event with its decoded payload, clipped to maxBytes (8 KB default). |
| get-stats | Counters, options, retained vs dropped. |
| set-options | paused, captureProtocol, maxEvents. |
| clear | Discard captured events. |
| channel-action | attach / detach / release. |
| emit-event | Deliver a synthetic message to the app's own subscribers, locally. |
list-events deliberately omits payload bodies — a single message can carry
hundreds of kilobytes, so a page of them would be unreadable. Find the id in a
listing, then read-event for the one that matters, which itself clips to 8 KB
unless you raise maxBytes. search covers decoded payload contents, which is
how you answer "which message carried this device id".
Only set-options, clear, channel-action and emit-event change anything,
and only channel-action touches Ably itself rather than what is recorded.
Firing an event without a backend
emit-event makes a realtime event arrive in the running app with no server,
no publish, and no network round trip:
npx rozenite agent avasapp/ably call --tool emit-event --session <id> \
--args '{"channel":"bid-orders","name":"ride_assignment","data":{"rideId":"r_42"}}'It hands a fabricated Ably.Message straight to the listeners the app itself
registered with subscribe(). It never publishes. A real publish would fan
the message out to every other client attached to that channel — another
developer's app, or a real device — and would need the network, so it could
neither run offline nor complete deterministically. Local delivery is the only
version that is both safe on a shared channel and usable in a test.
That also bounds what it can do: the app must already have subscribed, or there
is no listener to deliver to. delivered: 0 says exactly that, with a note
explaining whether the channel had no listener at all or every listener filtered
it out. Event-name and MessageFilter subscriptions are honoured as Ably would
honour them. The call returns once async listeners settle (up to 2s), so a
rejected promise is reported in failed/errors like a synchronous throw.
Injected events are recorded in the stream like any other, marked
injected: true and with their summary prefixed, so a listing never passes a
synthetic event off as one Ably delivered. They are not counted in the inbound
traffic counters, which describe only what Ably delivered.
Messages only — presence cannot be injected.
The name and the three arguments are a contract
emit-event is this plugin's implementation of a general capability — deliver an
inbound message the app never asked for — and Ably is only one transport that
could provide it. Push notifications and SSE deep links could satisfy the same
shape later.
Rozenite's AgentToolTraits has no capability field, so a wrapper that wants to
treat plugins as interchangeable providers has to discover them by convention:
enumerate the domains, then probe each for a tool named exactly emit-event.
That makes the short name a discovery key rather than a label, so it is not
decorated and will not be renamed. channel, name and data are the generic
arguments; everything else the tool accepts is optional Ably message metadata
that another transport would simply not have.
Nothing application-specific belongs in the signature. An app's own event
envelope is carried inside data, whose shape the tool never inspects.
For Node scripts built on @rozenite/agent-sdk, typed descriptors keep tool
names and argument shapes checked:
import { ablyTools } from '@avasapp/rozenite-plugin-ably/sdk'
const { items } = await session.callTool(ablyTools.listChannels, {
onlyErrored: true,
})Agent skill
Rozenite serves its own agent docs from the CLI (npx rozenite skills list) and
does not yet import skills from installed plugins, so this package ships one
instead — install it with npx skills add avas-app/rozenite-plugin-ably, or
point your agent at skills/ably-devtools/SKILL.md. It covers the tool surface
plus the semantics the raw output does not convey, such as why a channel the app
never subscribed to shows no inbound messages, and what the payload truncation
markers mean.
Protocol capture
Enabling protocol capture calls client.setLog({ level: 4 }). That method is
present at runtime but is not in ably-js's public typings, so it is
feature-detected — if it is missing, the panel disables the toggle and says so.
It is off by default because level 4 is genuinely noisy and slows the SDK.
Example app
example/ is a runnable Expo app that exercises the plugin with no Ably
account, no API key, and no login. The Ably client is a fake that produces
realistic traffic; everything else is real — it goes through the actual
useAblyDevTools hook and the actual Rozenite bridge, so the panel shows
exactly what a production app produces.
bun install && bun run build # in the repo root — the panel is served from dist/
cd example
bun install # symlinks the plugin (see scripts/link-plugin.mjs)
bun start # then press `j`, and pick the "Ably" tabThe app has buttons for the cases worth eyeballing: nested session events, a 25-message
burst, an outgoing publish, presence churn, a non-JSON payload, an oversized
payload that hits truncation, a channel failing with error 40160, a full
disconnect/recover cycle, and a channel release.
Two wiring details worth knowing if you adapt this setup:
- Rozenite discovers plugins from the project's declared
package.jsondependencies — it does not crawlnode_modules. Since the plugin lives one directory up,metro.config.jsnames it viainclude: ['@avasapp/rozenite-plugin-ably']. - Metro resolves the bare specifier to the plugin's TypeScript source, so SDK
edits hot reload. Panel edits still need
bun run buildin the root, because the panel is served fromdist/.
Development
bun install
bun test # instrumentation test suite
bun typecheck
bun run buildbun dev starts Rozenite's browser dev host on
localhost:8888 for quick panel iteration. Note that
rozenite.config.ts cannot import anything — Rozenite evaluates it with
new Function('module','exports', code) and no require in scope — so the dev
presets there are literal payloads. Realistic traffic lives in example/
instead, where it can import the real SDK.
To iterate against a real app:
bun link # in this repo
cd ../your-app && bun link @avasapp/rozenite-plugin-ably
ROZENITE_DEV_MODE=@avasapp/rozenite-plugin-ably bun devLicense
MIT
