HotCodePush

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.

src/release-attributes.ts
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:

KeyValueExample
hotcodepush.app.idappId from hotcodepush.jsona UUID
hotcodepush.channel.idgetChannel().ida UUID
hotcodepush.release.idcurrentRelease.ida UUID
hotcodepush.release.numbercurrentRelease.number, counted per channel42
hotcodepush.bundle.idcurrentRelease.bundleId, or embeddedBundleId on the store build's bundlea UUID
hotcodepush.bundle.versioncurrentRelease.bundleVersion, the label the upload gave the bundle1.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.

src/sentry.ts
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" ./dist

The 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.

src/otel-resource.ts
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:

ToolThe call
Firebase CrashlyticsCall setCustomKey(key, value) once per key.
Datadog RUMPut the keys on the global context, so every view and error carries them.
BugsnagCall addMetadata('hotcodepush', attributes).
PostHogCall register with the snake_case names, hotcodepush_release_number, so every event carries them.
Your backendSend 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 updateRolledBack event.
  • React Native SDK API documents getState, getChannel and the events in full. The Expo, Capacitor and Cordova references carry the same methods.

Last updated on

On this page