From 61eebdb1929ecd2f3cc87a6d8373926b098e0098 Mon Sep 17 00:00:00 2001 From: Floyd Kim Date: Tue, 25 Aug 2026 21:41:19 +0900 Subject: [PATCH 1/2] docs: move the telemetry callback reference out of the README The README's telemetry section carried the whole callback reference: every signature, the UpdateArchiveResult fields, and the fallback reasons. It now keeps a short paragraph and a link, and docs/telemetry-callbacks.md holds the reference - the callback table, what onUpdateArchiveResult reports, the UpdateArchiveResult and UpdateArchiveAttempt fields, the fallbackReason words, and why a fallback is not a failed update. What the README keeps of the archives is what the install guide needs: a table of the three archives in the order a client tries them, and the two exceptions to that order. --- README.md | 86 +++++++++---------------- docs/telemetry-callbacks.md | 121 ++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 56 deletions(-) create mode 100644 docs/telemetry-callbacks.md diff --git a/README.md b/README.md index 7010e698..20a93810 100644 --- a/README.md +++ b/README.md @@ -292,49 +292,12 @@ export default CodePush({ #### 4-1. Telemetry Callbacks -Please refer to the `CodePushOptions` type in [typings/react-native-code-push.d.ts](typings/react-native-code-push.d.ts) for more details. -- **onUpdateSuccess:** Triggered when the update bundle is executed successfully. -- **onUpdateRollback:** Triggered when there is an issue executing the update bundle, leading to a rollback. -- **onDownloadStart:** Triggered when the bundle download begins. -- **onDownloadSuccess:** Triggered when the bundle download completes successfully. -- **onSyncError:** Triggered when an unknown error occurs during the update process. (`CodePush.SyncStatus.UNKNOWN_ERROR` status) -- **onUpdateArchiveResult:** Triggered when an update that was published with a binary patch has been downloaded, with the release label and how the update archives went. Unlike the callbacks above, this one can also be passed to a single `sync()` call - see below. - -`onUpdateArchiveResult` is called with `{ status: "applied" | "fallback", archive: "binary-patch" | "asset-diff", fallbackReason?: string, totalDurationMs: number, attempts: [...] }`. -`archive` names the archive of the last attempt - the one the downloaded update came from, or the one given up on last - -and `attempts` retells every archive that was tried, in order, as `{ archive, fallbackReason?, durationMs, applyDurationMs? }`. -`totalDurationMs` times the whole patch path, from the first archive starting to download to the last attempt being -finished with, and an attempt's `applyDurationMs` times the applier rebuilding the bundle from that archive's patch - -absent when the attempt ended before the bundle was restored. -A `"fallback"` is not a failed update: the update is downloaded in full instead and installed as usual, so the result -is there to be observed and nothing more. The library neither stores it nor sends it anywhere - an app that wants it in -its telemetry sends it itself. A callback that throws never fails the update - the error is only logged to the console. -A fallback also shows in `downloadProgressCallback`: each following download reports a progress stream of its own that -counts `receivedBytes` from zero again, against its own `totalBytes`. - -Register `onUpdateArchiveResult` on `CodePush({ ... })` like the callbacks above and it covers every sync, whatever the `checkFrequency` is -and including the `CodePush.sync()` calls you make yourself. +`CodePushOptions` takes optional callbacks that report what an update did - when it was +downloaded, whether it ran or was rolled back, and which archive it came from. They are +purely for observation. -```typescript -export default CodePush({ - checkFrequency: CodePush.CheckFrequency.MANUAL, // or something else - releaseHistoryFetcher: releaseHistoryFetcher, - onUpdateArchiveResult: (label, result) => { - // Send it to your own telemetry, if you want it there. - }, -})(MyApp); -``` - -Pass it to a `sync()` call to override the registered one for that call - to tag the result of one particular sync, -for instance. - -```typescript -CodePush.sync({ - onUpdateArchiveResult: (label, result) => { - // Send it to your own telemetry, if you want it there. - }, -}); -``` +See [Telemetry callbacks](docs/telemetry-callbacks.md) for what each one receives and how to +register them. #### 4-2. Asset Diff Archives @@ -345,14 +308,32 @@ that version does not already have, and a manifest of the files it has to drop. copies the update it already has installed, applies those, and ends up holding exactly the contents of the full archive. -So one release has a full archive, a patch archive and up to `--diff-base-count` diff -archives, and a client starts from the smallest one it can use: the diff archive built -against the update it is running, when the release has one, and the patch archive when it -is running the bundle in the binary or an update this release was not diffed against. Each -attempt downloads one archive, and an archive that cannot be applied falls back to the -next. +A client tries the archives in this order and stops at the first one it can install: + +| Order | Archive | Available when | +|---|---|---| +| 1 | Asset diff | the release was diffed against the update the client is running. | +| 2 | Binary patch | always. It is built against the bundle in the app binary, which every client has. | +| 3 | Full | always. It needs nothing installed. | + +There are two exceptions to this order. + +**Not every client tries all three.** One with no asset diff to use - it is running the +bundle in the app binary, or an update this release was not diffed against - starts at the +binary patch. + +**A failed asset diff does not always reach the binary patch.** It moves on to that archive +only when the diff failed on its asset side: the merge with the installed update failing +(`asset_merge_failed`), or the merged contents failing the package hash +(`package_verification_failed`). Anything else the diff fails on lives in the bundle patch +both archives carry, so the binary patch would fail there the same way and the client goes +straight to the full archive. + +`onUpdateArchiveResult` reports every archive that was tried - see +[Telemetry callbacks](docs/telemetry-callbacks.md#what-onupdatearchiveresult-reports). Diff archives are published only when all three of these hold: + - the release is a binary patch release (`release --binary-bundle-path`), - `code-push.config.ts` implements `bundleDownloader`, so the CLI can fetch the earlier releases to diff against, - `--diff-base-count` is greater than `0` (it defaults to `3`). @@ -366,14 +347,7 @@ bundleDownloader: async (archive, platform, identifier = 'staging') => { }, ``` -A diff that cannot be applied falls back by where it failed. A failure on its asset -side - the merge with the installed update failing (`"asset_merge_failed"`), or the merged -contents failing the package hash (`"package_verification_failed"`) - moves on to the -patch archive, which carries every asset and depends on nothing installed. A failure in -the bundle patch both archives carry skips the patch archive for the full download -instead, so a client is never walked through two downloads that can only fail the same -way. Either way `onUpdateArchiveResult` reports the whole ladder in its `attempts`. See -[Asset diff archives](cli/README.md#asset-diff-archives) for what the release publishes. +See [Asset diff archives](cli/README.md#asset-diff-archives) for what the release publishes. ### 5. Configure the CLI Tool diff --git a/docs/telemetry-callbacks.md b/docs/telemetry-callbacks.md new file mode 100644 index 00000000..20453bd4 --- /dev/null +++ b/docs/telemetry-callbacks.md @@ -0,0 +1,121 @@ +# Telemetry Callbacks + +`CodePushOptions` takes optional callbacks that report what an update did. They exist to be +observed and nothing more: the library neither stores what they report nor sends it +anywhere, so an app that wants any of it in its telemetry sends it itself. Registering none +changes nothing about how updates behave, and a callback that throws is logged to the +console rather than failing the update it is reporting on. + +For the per-option reference, alongside every other option, see +[`CodePushOptions`](api-js.md#codepushoptions). + +## The callbacks + +| Callback | Called when | Receives | +|---|---|---| +| `onDownloadStart` | the download of an available update begins | `(label)` | +| `onDownloadSuccess` | that download has completed. The install happens afterwards, so this says nothing about whether the update could be installed | `(label)` | +| `onUpdateArchiveResult` | an update published with a binary patch has been downloaded, before it is installed | `(label, result)` | +| `onUpdateSuccess` | an installed update has run successfully, as of the [`notifyAppReady`](api-js.md#codepushnotifyappready) that marks it successful | `(label)` | +| `onUpdateRollback` | an installed update failed to run and was rolled back to the previous version | `(label)` | +| `onRolloutSkipped` | the device falls outside the latest release's active rollout, so the update check leaves that release out of the candidates | `(label)` | +| `onSyncError` | the sync ends in the [`SyncStatus.UNKNOWN_ERROR`](api-js.md#syncstatus) state | `(label, error)` | + +`label` is the release the report is about. `onSyncError` passes `"unknown"` instead when +the sync failed before a release was resolved. + +> [!NOTE] +> The typings declare a second `error` parameter on `onRolloutSkipped`, but the runtime only +> ever passes the label. + +## Registering them + +Pass them to the `CodePush({ ... })` wrapper from +["CodePush-ify" Your App](../README.md#4-codepush-ify-your-app). They run for every sync, +whatever the `checkFrequency` is, including the `CodePush.sync()` calls you make yourself. + +```typescript +export default CodePush({ + checkFrequency: CodePush.CheckFrequency.MANUAL, // or something else + releaseHistoryFetcher: releaseHistoryFetcher, + onUpdateSuccess: (label) => { + // Send it to your own telemetry, if you want it there. + }, + onUpdateArchiveResult: (label, result) => { + // Send it to your own telemetry, if you want it there. + }, +})(MyApp); +``` + +`onUpdateArchiveResult` is the only callback that you can also pass to an individual +`sync()` call. Passing it there overrides the registered one for that call - to tag the +result of one particular sync, for instance. + +```typescript +CodePush.sync({ + onUpdateArchiveResult: (label, result) => { + // Send it to your own telemetry, if you want it there. + }, +}); +``` + +## What `onUpdateArchiveResult` reports + +A release published with a binary patch offers the client patch archives to download in +place of the full one. See +[Asset Diff Archives](../README.md#4-2-asset-diff-archives) for what those are and when a +release carries them. This callback reports which of them the download came from, and what +happened to the ones it did not. + +### `UpdateArchiveResult` + +| Field | Type | Description | +|---|---|---| +| `status` | `"applied" \| "fallback"` | Whether one of the patch archives produced the update, or the full archive had to be downloaded instead. | +| `archive` | `UpdateArchive` | The archive of the last attempt: the one the update came from, or the last one given up on. | +| `fallbackReason` | `ArchiveFallbackReason` | Why the full archive had to be downloaded. Absent on `"applied"`, and when the last attempt ended in an error no applier has a word for. | +| `totalDurationMs` | `number` | How long the whole patch path took, from the first archive starting to download to the last attempt being finished with. The full download that follows a fallback is not part of it. | +| `attempts` | `UpdateArchiveAttempt[]` | Every archive that was tried, in the order it was tried. The full archive is never among them. | + +`UpdateArchive` is `"binary-patch"` or `"asset-diff"`. + +### `UpdateArchiveAttempt` + +| Field | Type | Description | +|---|---|---| +| `archive` | `UpdateArchive` | Which archive this attempt downloaded. | +| `fallbackReason` | `ArchiveFallbackReason` | Why this archive was given up on. Absent for the attempt the update came from. | +| `durationMs` | `number` | How long this attempt ran, whichever way it ended. | +| `applyDurationMs` | `number` | How long the applier took to rebuild the bundle from this archive's patch. Absent when the attempt ended before the bundle was restored. | + +Most downloads leave a single attempt. A second one appears when an asset diff failed on its +asset side - the merge with the installed update failing (`asset_merge_failed`), or the +merged contents failing the package hash (`package_verification_failed`). The client then +tries the binary patch, which carries every asset and depends on nothing installed. A diff +that failed in the bundle patch both archives carry skips the binary patch and goes straight +to the full archive, because it would fail there the same way. + +### `fallbackReason` + +Every platform's applier reports the same words, so a rollout can be judged by them +whichever platform it is running on. + +| Reason | Description | +|---|---| +| `base_bundle_unavailable` | The bundle inside the app binary could not be opened or read. | +| `base_hash_mismatch` | The bundle inside the app binary is not the one the patch was computed against. | +| `invalid_manifest` | The manifest is missing, malformed, points outside the archive, or asks for too much. | +| `unsupported_format` | The patch was produced by a format or a codec this client cannot apply. | +| `patch_apply_failed` | The applier refused the patch, or the restored bundle could not be written. | +| `target_verification_failed` | The restored bundle is not the one the manifest promised. | +| `asset_merge_failed` | The asset diff could not be merged with the installed update it was built against. | +| `package_verification_failed` | The update restored from the patch did not pass the checks that follow the restore. | + +### A fallback is not a failed update + +The update is downloaded in full instead and installed as usual. Nothing about the update +depends on what this callback is told. + +A fallback does show in `downloadProgressCallback`, though: each download after one reports +a progress stream of its own, counting `receivedBytes` from zero again against its own +`totalBytes`. From 296820d63d43929a9e9223bb0fb2f3b09fe5084d Mon Sep 17 00:00:00 2001 From: Floyd Kim Date: Tue, 25 Aug 2026 21:48:35 +0900 Subject: [PATCH 2/2] docs: add a Korean telemetry callback reference docs/telemetry-callbacks.ko.md carries the same sections and tables as the English reference, and the two link to each other from the top. --- docs/telemetry-callbacks.ko.md | 99 ++++++++++++++++++++++++++++++++++ docs/telemetry-callbacks.md | 2 + 2 files changed, 101 insertions(+) create mode 100644 docs/telemetry-callbacks.ko.md diff --git a/docs/telemetry-callbacks.ko.md b/docs/telemetry-callbacks.ko.md new file mode 100644 index 00000000..f469af3b --- /dev/null +++ b/docs/telemetry-callbacks.ko.md @@ -0,0 +1,99 @@ +# 텔레메트리 콜백 + +[English](telemetry-callbacks.md) + +`CodePushOptions`는 업데이트가 수행한 일을 보고하는 선택적 콜백을 받습니다. 이 콜백은 순전히 관찰을 위한 것이며 그 외의 역할은 없습니다. 라이브러리는 보고된 내용을 저장하거나 전송하지 않으므로, 텔레메트리에 기록하려면 앱에서 직접 전송해야 합니다. 콜백을 등록하지 않아도 업데이트 동작은 달라지지 않으며, 콜백이 예외를 던져도 해당 업데이트를 실패시키지 않고 콘솔에 기록합니다. + +다른 모든 옵션과 함께 각 옵션의 세부 내용을 확인하려면 [`CodePushOptions`](api-js.md#codepushoptions)를 참고하세요. + +## 콜백 + +| 콜백 | 호출 시점 | 전달값 | +|---|---|---| +| `onDownloadStart` | 사용 가능한 업데이트의 다운로드가 시작될 때 | `(label)` | +| `onDownloadSuccess` | 해당 다운로드가 완료될 때. 설치는 이후에 이뤄지므로, 업데이트를 설치할 수 있다는 뜻은 아닙니다. | `(label)` | +| `onUpdateArchiveResult` | binary patch로 배포된 업데이트가 다운로드된 뒤, 설치되기 전 | `(label, result)` | +| `onUpdateSuccess` | 성공 상태를 확정하는 [`notifyAppReady`](api-js.md#codepushnotifyappready) 호출을 기준으로, 설치된 업데이트가 정상 실행됐을 때 | `(label)` | +| `onUpdateRollback` | 설치된 업데이트가 실행에 실패해 이전 버전으로 롤백될 때 | `(label)` | +| `onRolloutSkipped` | 기기가 최신 릴리스의 활성 rollout 범위 밖이라 업데이트 검사에서 해당 릴리스를 후보에서 제외할 때 | `(label)` | +| `onSyncError` | sync가 [`SyncStatus.UNKNOWN_ERROR`](api-js.md#syncstatus) 상태로 종료될 때 | `(label, error)` | + +`label`은 보고 대상 릴리스입니다. 릴리스가 결정되기 전에 sync가 실패하면 `onSyncError`에는 대신 `"unknown"`이 전달됩니다. + +> [!NOTE] +> 타입 정의에서 `onRolloutSkipped`는 두 번째 `error` 매개변수를 선언하지만, 런타임은 항상 `label`만 전달합니다. + +## 등록 + +[앱에 CodePush 적용하기](../README.md#4-codepush-ify-your-app)의 `CodePush({ ... })` 래퍼에 콜백을 전달하면, `checkFrequency` 값과 무관하게 직접 호출한 `CodePush.sync()`를 포함한 모든 sync에서 실행됩니다. + +```typescript +export default CodePush({ + checkFrequency: CodePush.CheckFrequency.MANUAL, // or something else + releaseHistoryFetcher: releaseHistoryFetcher, + onUpdateSuccess: (label) => { + // Send it to your own telemetry, if you want it there. + }, + onUpdateArchiveResult: (label, result) => { + // Send it to your own telemetry, if you want it there. + }, +})(MyApp); +``` + +`onUpdateArchiveResult`는 개별 `sync()` 호출에도 전달할 수 있는 유일한 콜백입니다. 여기에 전달하면 그 호출에 한해 래퍼에 등록한 콜백을 덮어씁니다. 예를 들어 특정 sync의 결과만 별도로 태그할 때 사용할 수 있습니다. + +```typescript +CodePush.sync({ + onUpdateArchiveResult: (label, result) => { + // Send it to your own telemetry, if you want it there. + }, +}); +``` + +## `onUpdateArchiveResult`가 보고하는 내용 + +binary patch로 배포한 릴리스는 full 아카이브 대신 다운로드할 수 있는 patch 아카이브를 제공합니다. 아카이브의 종류와 릴리스에 포함되는 조건은 [Asset Diff Archives](../README.md#4-2-asset-diff-archives)를 참고하세요. 이 콜백은 업데이트를 어떤 아카이브에서 다운로드했는지와, 업데이트에 쓰이지 못한 나머지 아카이브에 어떤 일이 있었는지를 보고합니다. + +### `UpdateArchiveResult` + +| 필드 | 타입 | 설명 | +|---|---|---| +| `status` | `"applied" \| "fallback"` | patch 아카이브 중 하나로 업데이트를 만들었는지, 아니면 full 아카이브를 다운로드해야 했는지 나타냅니다. | +| `archive` | `UpdateArchive` | 마지막 시도 아카이브입니다. 업데이트를 가져온 아카이브이거나, 마지막으로 포기한 아카이브입니다. | +| `fallbackReason` | `ArchiveFallbackReason` | full 아카이브를 다운로드해야 했던 이유입니다. `"applied"`일 때는 없으며, 마지막 시도가 어느 applier도 이유 코드를 부여하지 못한 오류로 끝났을 때도 없습니다. | +| `totalDurationMs` | `number` | 첫 번째 아카이브 다운로드 시작부터 마지막 시도가 끝날 때까지 patch 경로 전체에 걸린 시간입니다. fallback 뒤에 이어지는 full 다운로드 시간은 포함하지 않습니다. | +| `attempts` | `UpdateArchiveAttempt[]` | 시도한 모든 아카이브를 시도한 순서대로 담습니다. full 아카이브는 포함하지 않습니다. | + +`UpdateArchive`는 `"binary-patch"` 또는 `"asset-diff"`입니다. + +### `UpdateArchiveAttempt` + +| 필드 | 타입 | 설명 | +|---|---|---| +| `archive` | `UpdateArchive` | 이 시도에서 다운로드한 아카이브입니다. | +| `fallbackReason` | `ArchiveFallbackReason` | 이 아카이브를 포기한 이유입니다. 업데이트를 가져온 시도에는 없습니다. | +| `durationMs` | `number` | 성공·실패와 관계없이 이 시도에 걸린 시간입니다. | +| `applyDurationMs` | `number` | applier가 이 아카이브의 patch로 번들을 복원하는 데 걸린 시간입니다. 번들을 복원하기 전에 시도가 끝나면 없습니다. | + +대부분의 다운로드에는 시도가 하나만 남습니다. asset diff가 asset 영역에서 실패하면 두 번째 시도가 생깁니다. 설치된 업데이트와 병합하지 못했거나(`asset_merge_failed`), 병합된 내용이 package hash 검증에 실패한 경우(`package_verification_failed`)입니다. 이때 클라이언트는 모든 asset을 포함하고 설치된 업데이트에 의존하지 않는 binary patch를 시도합니다. 반면 두 아카이브가 공유하는 bundle patch에서 asset diff가 실패하면 binary patch도 같은 방식으로 실패하므로 건너뛰고 곧바로 full 아카이브를 다운로드합니다. + +### `fallbackReason` + +모든 플랫폼의 applier는 같은 이유 코드를 보고하므로, 실행 플랫폼과 관계없이 rollout을 판단할 수 있습니다. + +| 이유 | 설명 | +|---|---| +| `base_bundle_unavailable` | 앱 바이너리 안의 번들을 열거나 읽을 수 없습니다. | +| `base_hash_mismatch` | 앱 바이너리 안의 번들이 patch를 생성할 때 기준으로 삼은 번들과 다릅니다. | +| `invalid_manifest` | manifest가 없거나 잘못됐거나, 아카이브 밖의 경로를 가리키거나, 허용 범위를 초과한 작업을 요청합니다. | +| `unsupported_format` | 이 클라이언트가 적용할 수 없는 형식 또는 codec으로 patch가 생성됐습니다. | +| `patch_apply_failed` | applier가 patch 적용을 거부했거나, 복원한 번들을 쓸 수 없습니다. | +| `target_verification_failed` | 복원한 번들이 manifest가 약속한 내용과 다릅니다. | +| `asset_merge_failed` | asset diff를 생성할 때 기준으로 삼았던 설치된 업데이트와 병합할 수 없습니다. | +| `package_verification_failed` | patch로 복원한 업데이트가 복원 뒤 검증에 실패했습니다. | + +### fallback은 업데이트 실패가 아닙니다 + +대신 full 아카이브를 다운로드하고 평소처럼 설치합니다. 이 콜백이 전달받는 내용은 업데이트 동작에 영향을 주지 않습니다. + +다만 fallback은 `downloadProgressCallback`에도 나타납니다. fallback 이후 이어지는 각 다운로드는 자체 progress stream을 가지며, 각각의 `totalBytes`를 기준으로 `receivedBytes`를 다시 0부터 계산합니다. diff --git a/docs/telemetry-callbacks.md b/docs/telemetry-callbacks.md index 20453bd4..d112eded 100644 --- a/docs/telemetry-callbacks.md +++ b/docs/telemetry-callbacks.md @@ -1,5 +1,7 @@ # Telemetry Callbacks +[한국어](telemetry-callbacks.ko.md) + `CodePushOptions` takes optional callbacks that report what an update did. They exist to be observed and nothing more: the library neither stores what they report nor sends it anywhere, so an app that wants any of it in its telemetry sends it itself. Registering none