Tag releases in your telemetry
Read the running release from the SDK and attach it to Sentry, OpenTelemetry or any tracker, so a crash names the bundle that caused it.
After an over-the-air release every device still reports the same store version to the error tracker. One version then covers many JavaScript bundles, and a new crash has no release to blame.
The SDK exposes the release a device is running, and the app attaches it to its own telemetry as a tag, attribute or property. The SDK depends on no telemetry package, so each tool takes a few lines of your own code.
Prerequisites
- Quickstart has run, so the SDK is installed and a release is live to read.
- The app already runs a tracker: Sentry, an OpenTelemetry setup or any tool that takes a key and a value.
1. Read the value
The call getState() answers currentRelease, the release the device runs, and getChannel() the channel it follows. The value is null while the device runs the bundle compiled into the store build, reported as embedded below. The app id is appId in hotcodepush.json, a public value the binary carries anyway.
import { HotCodePush } from "@hotcodepush/capacitor-live-updates";
import hotcodepush from '../hotcodepush.json';
export async function readReleaseAttributes(): Promise<Record<string, string>> {
const { currentRelease, embeddedBundleId } = await HotCodePush.getState();
const { id: channelId } = await HotCodePush.getChannel();
return {
'hotcodepush.app.id': hotcodepush.appId,
'hotcodepush.channel.id': channelId ?? 'unknown',
'hotcodepush.release.id': currentRelease?.id ?? 'embedded',
'hotcodepush.release.number': String(currentRelease?.number ?? 'embedded'),
'hotcodepush.bundle.id': currentRelease?.bundleId ?? embeddedBundleId ?? 'embedded',
'hotcodepush.bundle.version': currentRelease?.bundleVersion ?? 'embedded',
};
}The six keys are HotCodePush's own attribute names, one per value:
| Key | Value | Example |
|---|---|---|
hotcodepush.app.id | appId from hotcodepush.json | a UUID |
hotcodepush.channel.id | getChannel().id | a UUID |
hotcodepush.release.id | currentRelease.id | a UUID |
hotcodepush.release.number | currentRelease.number, counted per channel | 42 |
hotcodepush.bundle.id | currentRelease.bundleId, or embeddedBundleId on the store build's bundle | a UUID |
hotcodepush.bundle.version | currentRelease.bundleVersion, the label the upload gave the bundle | 1.4.2 |
The release number is what a person reads, and the bundle id is what a source map matches. A tool that wants snake_case takes the same names with underscores, hotcodepush_release_number.
Read the value once at start, before the first span or event. A release becomes the running one only across a reload, so the value cannot go stale while the app runs. The one place to read it again is an updateRolledBack handler, which Roll back a release shows.
2. Sentry
Keep release as the store version and give dist the bundle id. Sentry links an event to the uploaded source maps by that pair, and the release number goes beside it as a tag.
Sentry initializes first and learns the release a moment later. So a crash in the first instants is still captured, with the store version and without the tag. The body takes Sentry from the package the app already initializes, @sentry/capacitor or @sentry/react-native.
import { HotCodePush } from "@hotcodepush/capacitor-live-updates";
// import * as Sentry from the Sentry package your app uses
let dist: string | undefined;
Sentry.init({
dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
release: 'acme-app@1.4.2', // the store version
beforeSend(event) {
event.dist = dist;
return event;
},
});
HotCodePush.getState().then(({ currentRelease, embeddedBundleId }) => {
dist = currentRelease?.bundleId ?? embeddedBundleId ?? undefined;
Sentry.setTag('hotcodepush.release.number', String(currentRelease?.number ?? 'embedded'));
});In CI, upload the bundle's source maps against the same pair. The --json output of release create is an array with one release per channel, so the bundle id is the first entry's:
BUNDLE_ID=$(npx hotcodepush release create --yes --json | jq -r '.[0].bundleId')
npx sentry-cli sourcemaps upload --release "acme-app@$VERSION" --dist "$BUNDLE_ID" ./distThe CLI leaves *.map files and dotfiles out of every upload, so no device downloads a map.
3. OpenTelemetry
Resource attributes are fixed when the provider is built, so read the release first and build the provider after. The resource comes from resourceFromAttributes in @opentelemetry/resources, and ATTR_SERVICE_NAME and ATTR_SERVICE_VERSION from @opentelemetry/semantic-conventions carry the app and its store version beside the six keys.
import { HotCodePush } from "@hotcodepush/capacitor-live-updates";
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
import { readReleaseAttributes } from './release-attributes';
export async function buildResource() {
const { binaryVersion } = await HotCodePush.getDevice();
return resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'acme-app',
[ATTR_SERVICE_VERSION]: binaryVersion,
...(await readReleaseAttributes()),
});
}Pass the resource to the provider as its resource option. If the provider cannot wait for the promise, keep the resource static and set the same attributes on every span from a span processor's onStart instead.
4. Any other tool
Every tool that takes a string key and a string value takes the six keys, read once at start as in step 1:
| Tool | The call |
|---|---|
| Firebase Crashlytics | Call setCustomKey(key, value) once per key. |
| Datadog RUM | Put the keys on the global context, so every view and error carries them. |
| Bugsnag | Call addMetadata('hotcodepush', attributes). |
| PostHog | Call register with the snake_case names, hotcodepush_release_number, so every event carries them. |
| Your backend | Send the release number in a request header, logged beside the request. |
Next steps
- Ship every commit has the pipeline that releases every commit, where the source-map upload of step 2 belongs.
- Roll back a release says what to do once the graph names a release, and covers the
updateRolledBackevent. - React Native SDK API documents
getState,getChanneland the events in full. The Expo, Capacitor and Cordova references carry the same methods.
Last updated on
Switch channels by link
Register a URL scheme, handle the link in React Native, Expo, Capacitor or Cordova and call setChannel with the channel id it carries.
CI/CD
Release every merge to production from your pipeline with one CLI line, and keep the three disciplines that make shipping every commit safe.