client agent
React Native
An app has no server-side span to compare against, so a refused call or an empty response is only visible from the device. This is the agent that makes it visible.
- package
@sixty-sh/react-nativeon npm- runs on
- iOS and Android. Roughly 2.6µs of blocking cost per request it observes.
- source
- packages/react-native
Installing it
iOS and Android. API shape and latency, errors and crashes, render loops — with a public key.
The install is written as a prompt for the coding agent you already have open, not as a checklist for you. That is deliberate: it names what must be true when the install is finished rather than which files to edit, because where the code goes depends on the framework and putting it in the wrong place fails silently. An agent can read your repository and work that out; a paragraph on a documentation page cannot.
The same text is what install_sixty returns through the MCP server and what the collector serves at /v1/setup?kind=react-native. There is one copy of it.
the React Native install, in full
Add sixty.sh monitoring to this React Native app. It measures API call shape
and latency, JavaScript errors and crashes, and runaway re-render loops, and
reports anonymously.
1. Install @sixty-sh/react-native. Also install
@react-native-async-storage/async-storage if it is not already present —
without it, telemetry buffered when the user backgrounds the app is lost,
and that is how most sessions end.
2. Call init() once, at the top of the entry file (index.js or App.tsx),
before anything else imports:
import { init, composeRelease } from "@sixty-sh/react-native"
init({
key: "sixty_pk_YOUR_KEY",
service: "my-app",
endpoint: "https://ingest.sixty.sh",
release: composeRelease({ version: APP_VERSION, build: BUILD_NUMBER }),
})
Work out where the version and build number actually come from in THIS
project — expo-constants, react-native-device-info, a generated file, or a
constant somebody maintains by hand. Do not add a dependency just for this;
if none exists, hardcode the current version and say so in your summary.
IF this app ships over-the-air updates (expo-updates or CodePush), the
release MUST also include the update id:
composeRelease({ version, build, updateId: Updates.updateId })
Without it every OTA bundle reports as the same release, and since OTA is
how most changes ship, almost nothing can ever be compared. Nothing errors;
the feed just stays empty.
3. Add the Babel plugin to babel.config.js:
plugins: ["@sixty-sh/react-native/babel"]
Then start Metro with --reset-cache once. Metro caches transformed modules
and will not notice the plugin list changed, so without this the build
succeeds and the transform is silently not applied.
4. Wire the navigator, or every measurement in the app is filed under one
route and no per-screen finding is possible. Find the NavigationContainer:
import { createNavigationTracker } from "@sixty-sh/react-native/navigation"
const tracker = createNavigationTracker()
<NavigationContainer onReady={tracker.onReady} onStateChange={tracker.onStateChange}>
If this app does not use React Navigation, call setRoute("ScreenName") from
wherever it does track the current screen instead.
5. If the app has React error boundaries, call reportError(error) from
componentDidCatch. React calls the boundary instead of any global handler,
so errors caught there are invisible to the agent otherwise — and those are
the ones that leave a user looking at a fallback screen.
Constraints — correctness requirements, not style preferences:
- Do NOT wrap or replace fetch. React Native's fetch is implemented on top of
XMLHttpRequest, which the agent already instruments; adding a fetch wrapper
makes every request count twice and nothing will look wrong.
- Do NOT change any existing screen, hook, or network call. This is
instrumentation only; no behaviour should change.
- The key above is PUBLIC by design: write-only and pinned to one service.
Paste it literally. Do not move it into an environment variable.
- Do NOT add any analytics library, user id, session id, or device id. This
agent is deliberately anonymous and must stay that way.
When you are done, tell me where you found the version number, whether this
app ships OTA updates, and whether you cleared the Metro cache. Findings need
two released builds with traffic under each before anything can be compared.It needs a public key — pinned to your origins, because an app with no server of its own has nowhere to keep a secret.
What it measures
| signal | unit | what it means |
|---|---|---|
client_error | of views | this error is being thrown in real users’ browsers |
auth_failures | — | The server is turning these away on permission grounds rather than failing. People see an empty page or a save that quietly does nothing. |
silent_empty | — | The query runs and succeeds, and returns no rows where it used to return plenty. Nothing reports an error, so the page just renders blank — this is what a broken permission rule looks like from the outside. |
latency | ms per call | this operation takes longer end to end than it used to |
errors | error rate | a larger fraction of calls are throwing |
runaway | calls per minute | this operation is being called far more often than anything triggers it |
Where it hooks in
- React Navigation — Wired to the navigator, so measurements are filed under a screen. Skipping this leaves every number under one route.
- Your own components — A Babel plugin. Metro caches transformed modules and does not notice a new plugin list, so the cache has to be cleared or the build silently does nothing.
What only this one does
- Network signals from the device — Auth refusals and successful-but-empty responses, which on a phone are the only place either is observable.
- OTA-aware releases — The over-the-air update id is part of the release identifier. Without it every bundle pushed over the air is the same release and comparison has nothing to compare.
What it cannot do
- A public key, because an app has no origin to proxy through and nowhere for a secret to live. Unlike the browser tier there is no better mode being given up.
- dead_interaction is keyed to button, a and form, and does not yet know about Pressable or onPress. effect_loop and missing_cleanup are React rules and fire on this source unchanged.
Configuration
Every agent reads the same four variables, and DRIFT_* still answers everywhere SIXTY_* does — the product was renamed and that name is not ours to retire from other people’s deployments.
SIXTY_API_KEY | Without it the agent stays inert and says so. It never guesses, never retries against an unknown endpoint, and never throws. |
|---|---|
SIXTY_SERVICE | What to call this service. Defaults to the project name where one is legible. |
SIXTY_RELEASE | The one that matters most. Picked up automatically on Vercel, Render, Railway, Fly, Heroku and GitHub Actions; set it to the commit SHA anywhere else. Without it every measurement lands in a single nameless bucket and no comparison can ever be made. |
SIXTY_ENDPOINT | Where to report. Defaults to http://localhost:4319, which is right on a laptop and wrong the moment the app is served to anyone else. |
The rest — flush interval, sample rate, what to instrument — is in the package’s own README, which is where it can stay true as the agent changes.