Insights Plugin
Report launches, downloads, applies, recoveries, and update failures to your server's Insights with the built-in `insights()` client plugin.
Add the plugin
insights() is a client plugin
exported from @hot-updater/react-native. It reports nothing until you add it
to plugins in HotUpdater.init; the instance
init returns then has hotUpdater.insights:
import { HotUpdater, insights } from "@hot-updater/react-native";
export const hotUpdater = HotUpdater.init({
baseURL: "<your-update-server-url>",
requestHeaders: {
"x-api-key": "<your-api-key>",
},
plugins: [insights()],
});It sends each report to POST /events under baseURL, with the
requestHeaders and requestTimeout of init, to a server that runs the
insights() server plugin,
as the managed servers do. The Insights guide covers
verifying reports and reading them.
Options
| Option | Type | Default | Description |
|---|---|---|---|
debug | boolean | false | Also report from debug builds, where __DEV__ is true. |
A debug build sends nothing by default, so development sessions stay out of
production Insights. To test reporting from one, create the plugin with
insights({ debug: true }). The option has no effect in a release build.
Set the user
Attach your app's ID for the signed-in user to later reports:
import { hotUpdater } from "./hotUpdater";
// After sign-in, or when the app restores a session:
hotUpdater.insights.setUser({ userId: "user-123" });
// On sign-out:
hotUpdater.insights.setUser(null);setUser stores the ID on the device, and every later report carries it as
userId until setUser(null) clears it. It sends nothing by itself; a
session restored right after init is still on the first launch report. A
number becomes a string, and an empty ID clears the user. A new user counts as
a change for launch reports; the
installation ID stays the same.
In the Console, Insights → All events searches by user ID or installation ID. Choose an ID that fits your data policy; see Security.
What it reports
| Report | Sent when |
|---|---|
UNCHANGED | The app launched without applying or recovering an update, or an update check found nothing to install. |
UPDATE_DOWNLOADED | The SDK downloaded, verified, and staged a bundle. The app runs the previous bundle until it restarts. |
UPDATE_APPLIED | A staged update became active at launch. |
RECOVERED | Native crash recovery rolled back a failed update at launch. |
UPDATE_FAILED | An update check, download, or install failed. |
A same-bundle Release adoption, which needs no download, is also reported as
UNCHANGED, with the Release it replaced as fromReleaseId.
init reports the launch once native launch verification finishes.
Reports of checks, downloads, and failures always follow the launch report of
their JavaScript runtime.
Downloads, applies, and recoveries are always sent, unless reporting is paused. Launch reports and failures follow the rules below.
Launch reports
An installation sends UNCHANGED at most once per UTC day. A later launch or
update check on the same UTC day sends nothing, unless the channel, app
version, bundle, Release, or user ID changed since the day's last report, or a
download or failure report reached the server since then.
An UPDATE_APPLIED or RECOVERED report counts as that day's report for the
bundle it started. After a download, the same JavaScript runtime sends no
UNCHANGED, so the server keeps showing the update as waiting to apply. A
launch report the server did not store, such as one that failed its retries,
is sent again at the next launch or check.
The server applies the same rule, so Console launch counts are daily active installations. See how the server counts reports.
HTTP responses
The plugin keeps the latest catalog or artifact response (200, 304, or an
error) and attaches it to the next report it sends anyway, as
metadata.httpResponse. It sends no extra request or report, and keeps only
one response per app version and channel, so it is a snapshot, not a request
log.
The snapshot has resource, path, status, the raw body (up to 4 KiB,
with bodyTruncated), and receivedAtMs. An empty body is "", and one that
could not be read is null. Headers, query parameters, native file downloads,
and Insights' own requests are left out.
Update failures
UPDATE_FAILED says where and why an update failed. It carries the
onUpdateError details
the SDK knows:
| Detail | Values |
|---|---|
stage | check: the Release Catalog request. download: transferring and verifying files. install: patching, extracting, and moving them into place. |
reason | network, http, invalid_response, hash_mismatch, signature, patch, extract, storage, or unknown. |
errorMessage | The original error message. JSON-encoded text is capped at 2 KiB. |
errorStack | The original stack trace, when available. JSON-encoded text is capped at 4 KiB. |
resource | catalog, artifact, manifest, file, patch, or archive. |
httpStatus | The response status of an http failure. |
originCode | The storage origin's error code of an http failure, such as AccessDenied, NoSuchKey, or ExpiredToken. |
transport | Why no response arrived: timeout, dns, tls, connection, offline, or cancelled. |
- An installation reports each combination of stage, reason, target bundle, and error message at most once per UTC day.
- The report's
eventIdis derived from the installation, the UTC day, and that combination, so a server that stores reports by ID keeps a repeated failure once. - A check that failed with a network error is not reported: the device was most likely offline.
- A failure still waiting to be sent, or being retried, is dropped once the
same runtime reports
UPDATE_DOWNLOADEDorUPDATE_APPLIEDfor the same target bundle.
A check that finds no Release for its scope, a bundle in the device's crash history, and a selection that a newer check replaced are not update failures, so the plugin reports none of them.
The Console shows the original message, an expandable stack, and the latest server response beside it. These are errors the device saw, not server logs. Error text is not redacted; only oversized text is cut, with an ellipsis.
Delivery
Reporting never delays startup or an update, and it does not trigger
rollback. A report that fails with a network error, a timeout, 429, or a
5xx is retried in the background: up to three attempts in all, about 1
second and then 2 seconds apart, or after the server's Retry-After (at most
30 seconds). Each attempt has its own requestTimeout, and every attempt
carries the same eventId, a UUIDv7, so the server counts a retried report
once. A report refused with another status, such as 400, is not retried.
Reports leave one at a time in the order the app made them, so a retry never lands after a later report. The plugin logs a warning when a report cannot be delivered. Retries live in memory: a report still pending when the app closes or reloads is lost.
When the server has Insights off
A server without the insights() server plugin has no POST /events route
and answers 404. After that answer, the plugin sends nothing more in that
JavaScript runtime, and nothing for 24 hours. Reports made during the pause
are dropped, not sent later.
A later JavaScript runtime can send reports once at least 24 hours have
passed since the 404. Once the server stores a report, reporting continues
as usual. Enabling insights() on the server does not require an app update,
but the runtime that received the 404 must restart before reporting resumes.
Report format
Each report is one JSON body. This failed download shows every top-level field:
{
"eventId": "01929f4e-2b7c-7a51-9d3e-5c1f0a6b8e21",
"type": "UPDATE_FAILED",
"installId": "5f1c2a9e-7b3d-4c8e-a1f0-2d6b9e4c7a13",
"userId": "user-123",
"platform": "ios",
"appVersion": "1.4.0",
"channel": "production",
"cohort": "412",
"fingerprintHash": null,
"sdkVersion": "1.0.0",
"minBundleId": "01929d00-0000-7000-8000-000000000000",
"fromBundleId": "01929e11-6c2d-7f40-b8a5-3e7d1c9f0a42",
"fromReleaseId": "01929e11-7a90-7c3e-9f21-8b4d2e6a1c57",
"toBundleId": "01929f2a-1d5e-7b8c-a3f6-4c9e2b7d0f18",
"toReleaseId": "01929f2a-2e71-7d4a-8c05-6f1b3a9e2d64",
"updateStrategy": "appVersion",
"metadata": {
"failure": {
"stage": "download",
"reason": "http",
"resource": "file",
"httpStatus": 403,
"originCode": "ExpiredToken"
}
}
}| Field | Value |
|---|---|
userId | The ID from setUser; absent without one. |
minBundleId | The bundle identifier embedded in the native build. |
fromBundleId, fromReleaseId | The bundle and Release the app ran before the transition; for UPDATE_FAILED, the ones it runs. For UNCHANGED, fromBundleId is null, and fromReleaseId is the Release a same-bundle adoption replaced, or null. |
toBundleId, toReleaseId | The bundle and Release the transition moved to; for UNCHANGED, the ones the app runs; for UPDATE_FAILED, the update's target, or the running bundle for a failed check. toReleaseId is null without a Release. |
updateStrategy | appVersion or fingerprint; null for UNCHANGED. |
metadata | Optional httpResponse snapshot for any report, plus details by report type below. |
| Report | metadata |
|---|---|
UPDATE_DOWNLOADED | delivery: patch, manifest, or archive, as in onBundleDownloaded; patchFallback: true when a patch was tried but a file or the archive was downloaded instead. |
UPDATE_FAILED | failure: stage and reason, plus errorMessage, errorStack, resource, httpStatus, originCode, and transport when known. |
See how the server accepts reports for its size limit and status codes.
Turn reporting off
Leave insights() out of plugins. The app then sends no Insights requests;
native launch verification and crash recovery still run. Reports the server
already stored stay there.