reis-mobile 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +23 -0
- package/README.md +54 -27
- package/agents/android-architect.md +3 -3
- package/agents/android-performance-engineer.md +1 -1
- package/agents/flutter-architect.md +3 -3
- package/agents/flutter-performance-engineer.md +1 -1
- package/agents/ios-architect.md +3 -3
- package/agents/ios-performance-engineer.md +1 -1
- package/agents/kmp-architect.md +3 -3
- package/agents/kmp-performance-engineer.md +1 -1
- package/agents/lead-mobile.md +3 -0
- package/agents/mobile-release-engineer.md +4 -4
- package/agents/mobile-staff-engineer.md +1 -1
- package/agents/rn-architect.md +3 -3
- package/agents/rn-performance-engineer.md +1 -1
- package/bin/reis-mobile.mjs +24 -1
- package/commands/reis-mobile.md +7 -2
- package/commands/release.md +46 -0
- package/core/release/context.mjs +11 -0
- package/core/router/intents.mjs +15 -0
- package/package.json +1 -1
- package/skills/mobile-accessibility/SKILL.md +3 -0
- package/skills/mobile-accessibility/references/localization.md +77 -0
- package/skills/mobile-observability/SKILL.md +137 -0
- package/skills/mobile-observability/references/android.md +105 -0
- package/skills/mobile-observability/references/flutter.md +86 -0
- package/skills/mobile-observability/references/ios.md +88 -0
- package/skills/mobile-observability/references/kotlin-multiplatform.md +76 -0
- package/skills/mobile-observability/references/react-native.md +62 -0
- package/skills/mobile-offline-sync/SKILL.md +132 -0
- package/skills/mobile-offline-sync/references/android.md +123 -0
- package/skills/mobile-offline-sync/references/flutter.md +103 -0
- package/skills/mobile-offline-sync/references/ios.md +103 -0
- package/skills/mobile-offline-sync/references/kotlin-multiplatform.md +114 -0
- package/skills/mobile-offline-sync/references/react-native.md +108 -0
- package/skills/mobile-performance/SKILL.md +88 -0
- package/skills/mobile-performance/references/android.md +96 -0
- package/skills/mobile-performance/references/background.md +87 -0
- package/skills/mobile-performance/references/flutter.md +101 -0
- package/skills/mobile-performance/references/ios.md +82 -0
- package/skills/mobile-performance/references/kotlin-multiplatform.md +62 -0
- package/skills/mobile-performance/references/react-native.md +85 -0
- package/skills/mobile-release/SKILL.md +11 -22
- package/skills/mobile-release/references/billing.md +92 -0
- package/skills/mobile-release/references/readiness.md +88 -0
- package/skills/mobile-release/scripts/verdict.mjs +59 -0
- package/skills/mobile-security/SKILL.md +3 -0
- package/skills/mobile-security/references/auth.md +92 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Audit mobile release readiness with per-target evidence, store quality gates and a GO/GO WITH RISKS/NO-GO verdict
|
|
3
|
+
argument-hint: "[request, version, variant or target store/track]"
|
|
4
|
+
allowed-tools: ["Bash(node:*)", "Bash(git:*)", "Read", "Grep", "Glob", "WebFetch", "WebSearch"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# reis-mobile release
|
|
8
|
+
|
|
9
|
+
Arguments received: `$ARGUMENTS`
|
|
10
|
+
|
|
11
|
+
## 1. Prepare context
|
|
12
|
+
|
|
13
|
+
Run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/reis-mobile.mjs" release --dir "$PWD" -- "<request>"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Pass the request as a single literal argument after `--`, or omit it when empty. Use proper shell escaping: double quotes alone do not protect `$()`, backticks or embedded quotes. Never evaluate the request as shell code. The CLI forces `release` intent even if the request mentions a build failure. Stop and show the error if context preparation fails. Exit 0 means context preparation succeeded, not that a release is ready.
|
|
20
|
+
|
|
21
|
+
Use `Project` (`detection.projectDir` in JSON) as the app root and `context.repoRoot` for git-relative paths. The CLI lists candidate release platforms and reference paths (`release.platforms` and `release.references`); they are unverified inventory, not a verdict. Inspect untracked files and required files omitted by diff truncation. The git diff alone is not release evidence.
|
|
22
|
+
|
|
23
|
+
If the resolved directory does not exist or `detection.stack` is `unknown`, report NO-GO with unresolved identity/targets and the exact app path needed. Do not use a stack inferred from the prompt as evidence of an app on disk. Feed an empty `targets` array to the verdict helper rather than inventing artifacts.
|
|
24
|
+
|
|
25
|
+
## 2. Bind the release to the app
|
|
26
|
+
|
|
27
|
+
Read in order:
|
|
28
|
+
|
|
29
|
+
1. `${CLAUDE_PLUGIN_ROOT}/docs/agent-context.md`; build or reuse the Project brief from the resolved app root.
|
|
30
|
+
2. `${CLAUDE_PLUGIN_ROOT}/agents/<Agent>.md`, normally `mobile-release-engineer`.
|
|
31
|
+
3. `${CLAUDE_PLUGIN_ROOT}/skills/<skill>/SKILL.md` for each routed skill, including `mobile-release`.
|
|
32
|
+
4. `${CLAUDE_PLUGIN_ROOT}/skills/mobile-release/references/readiness.md` and each relevant release reference from the CLI output.
|
|
33
|
+
|
|
34
|
+
Apply the selected role directly, or pass the existing brief to that specialist when delegation is available and appropriate. Do not repeat project discovery. Resolve the actual app ID, flavor/scheme/environment, version/build, source revision, store/track and built artifact for each target. For every field, record its source and distinguish configured values from values verified in the built artifact. Without an explicit request, inventory configured mobile targets and existing artifacts; leave ambiguous destinations and missing artifacts unknown.
|
|
35
|
+
|
|
36
|
+
Flutter, React Native/Expo and Kotlin Multiplatform shipping to Android and iOS require both native release references and separate artifact checks. Confirm hosts/targets in the build configuration; a KMP framework build is not an iOS archive. Honor an explicitly limited store scope and name exclusions. Native folders generated by Expo need configuration inspection, not prebuild during the audit.
|
|
37
|
+
|
|
38
|
+
## 3. Audit, evaluate and report
|
|
39
|
+
|
|
40
|
+
Follow the required gates, evidence rules, verdict helper and report format in `readiness.md`. Reconcile local facts with existing artifact metadata, actual CI results and current official store requirements. Do not claim a store or CI check passed from local configuration alone. If external information is unavailable, identify the exact confirmation needed.
|
|
41
|
+
|
|
42
|
+
Run `${CLAUDE_PLUGIN_ROOT}/skills/mobile-release/scripts/verdict.mjs` with the actual per-target gates on stdin as described in the reference. Missing evidence must remain unknown. Report its GO, GO WITH RISKS or NO-GO verdict with release identity, gate evidence, blockers, risks, unknowns, next actions and rollout/rollback. Its exit status is not a readiness verdict.
|
|
43
|
+
|
|
44
|
+
The audit is read-only by default: no builds/tests, version bump, tag, upload, signing change or store-setting change. An explicit release execution request follows the selected agent's scope rules and prior authorization; never execute a publishing lane just because it exists. Distinguish commands inspected/prepared from those actually executed.
|
|
45
|
+
|
|
46
|
+
Write user-facing prose in the CLI's `Language`: `en` is English, `pt` is Brazilian Portuguese; with `-` or `null`, follow the user's language. Keep paths, gate IDs, verdicts and quoted output unchanged.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { PLUGIN_ROOT } from '../paths.mjs';
|
|
4
|
+
|
|
5
|
+
/** Detection suggests references, never proves store targets or release readiness. */
|
|
6
|
+
export function releaseContext(detection) {
|
|
7
|
+
const platforms = detection.platforms.filter((platform) => ['android', 'ios'].includes(platform));
|
|
8
|
+
const references = detection.stack === 'unknown' ? [] : [...new Set([detection.stack, ...platforms])]
|
|
9
|
+
.map((stack) => join(PLUGIN_ROOT, 'skills/mobile-release/references', `${stack}.md`));
|
|
10
|
+
return { platforms, references, evidenceStatus: 'unverified', verdict: null };
|
|
11
|
+
}
|
package/core/router/intents.mjs
CHANGED
|
@@ -28,6 +28,21 @@ export const INTENTS = {
|
|
|
28
28
|
'feature first', 'estrutura de pastas', 'state management', 'gerenciamento de estado',
|
|
29
29
|
'injecao de dependencia', 'dependency injection', 'design pattern', 'camada',
|
|
30
30
|
],
|
|
31
|
+
offline: [
|
|
32
|
+
'offline', 'off-line', 'sem conexao', 'sem internet', 'sem rede', 'modo aviao',
|
|
33
|
+
'airplane mode', 'sincroniza', 'sync', 'dessincroniz', 'out of sync', 'conflito',
|
|
34
|
+
'conflict', 'idempot', 'fila de operac', 'operation queue', 'outbox', 'reconect',
|
|
35
|
+
'reconnect', 'local-first', 'local first', 'cache local', 'last write wins',
|
|
36
|
+
'ultima escrita vence', 'replica',
|
|
37
|
+
],
|
|
38
|
+
observability: [
|
|
39
|
+
'observab', 'telemetri', 'telemetry', 'opentelemetry', 'crashlytics', 'sentry',
|
|
40
|
+
'datadog', 'new relic', 'firebase analytics', 'analytics', 'monitorament', 'monitoring',
|
|
41
|
+
'structured log', 'log estruturad', 'logging', 'alerta', 'alert', 'sampling',
|
|
42
|
+
'amostragem', 'distributed trac', 'trace distribu', 'correlation id', 'correlation-id',
|
|
43
|
+
'crash-free', 'crash free', 'error reporting', 'relatorio de erro', 'breadcrumb',
|
|
44
|
+
'slo', 'sli',
|
|
45
|
+
],
|
|
31
46
|
release: [
|
|
32
47
|
'release', 'publica', 'publish', 'loja', 'app store', 'play store', 'testflight',
|
|
33
48
|
'lancamento', 'submission', 'store review', 'rejeita', 'reject',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reis-mobile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "AI agents and skills for Flutter, Android, iOS, React Native and Kotlin Multiplatform — a Claude Code and Codex plugin for reviews, debugging, tests, architecture, security and Firebase.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,9 +18,12 @@ One accessibility process for every mobile stack. This file holds what is true e
|
|
|
18
18
|
| Native iOS: SwiftUI or UIKit | [references/ios.md](references/ios.md) |
|
|
19
19
|
| React Native or Expo | [references/react-native.md](references/react-native.md) |
|
|
20
20
|
| Kotlin Multiplatform with shared Compose UI or native host UI | [references/kotlin-multiplatform.md](references/kotlin-multiplatform.md) |
|
|
21
|
+
| Localization, plurals, formatting and right-to-left layout, on any stack | [references/localization.md](references/localization.md) |
|
|
21
22
|
|
|
22
23
|
In Flutter and React Native apps, the native reference applies only to screens or components written natively (platform views, native modules with UI).
|
|
23
24
|
|
|
25
|
+
Localization is reviewed with accessibility because it breaks the same things — a layout that assumed one text length, a fixed-height label, text baked into an image. When the app ships in more than one language, or in a right-to-left one, read [references/localization.md](references/localization.md) alongside this file.
|
|
26
|
+
|
|
24
27
|
## 2. What a screen reader user needs
|
|
25
28
|
|
|
26
29
|
Every interactive element exposes four things to the accessibility tree. Check them in this order:
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Localization and internationalization
|
|
2
|
+
|
|
3
|
+
Applies to every stack. Localization sits next to accessibility because it breaks the same things: a layout that assumed one text length, a string that assumed one grammar, and a date that assumed one calendar. An app that survives the largest accessibility font size usually survives German; one that does not, does not.
|
|
4
|
+
|
|
5
|
+
## The rules that hold on every platform
|
|
6
|
+
|
|
7
|
+
**No user-visible string in code.** Every string lives in the platform's resource system, keyed, with the source language as the base. A hard-coded string is not translatable and will not be found by review; grep for string literals inside UI code as the first pass of an audit.
|
|
8
|
+
|
|
9
|
+
**Never build a sentence by concatenation.** `"You have " + n + " messages"` cannot be translated: word order, pluralization and grammatical gender all differ. Use a parameterized string with named placeholders, so the translator can reorder them:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
"order_shipped_on": "Your order {orderId} ships on {date}"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Plurals are a category, not a number.** English has two forms; Polish has four; Arabic has six; Japanese has one. `if (n == 1)` is wrong in most languages. Every platform implements the CLDR plural categories (`zero`, `one`, `two`, `few`, `many`, `other`) — use them:
|
|
16
|
+
|
|
17
|
+
| Stack | Mechanism |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Android | `<plurals>` resources and `getQuantityString` |
|
|
20
|
+
| iOS | A stringsdict / the `String Catalog`'s plural variations, with `NSLocalizedString` |
|
|
21
|
+
| Flutter | ICU message syntax in ARB files (`{count, plural, =0{...} one{...} other{...}}`) with `flutter_localizations` and `gen_l10n` |
|
|
22
|
+
| React Native | `i18next` with the ICU or plural suffix conventions, `react-intl`, or `Intl.PluralRules` directly |
|
|
23
|
+
| Kotlin Multiplatform | Compose Multiplatform resources, or host resources through `expect`/`actual` — check the plural support of the version in use |
|
|
24
|
+
|
|
25
|
+
**Never format data by hand.** Dates, times, numbers, currencies, percentages, lists and relative times all have locale rules, and `"$" + amount` is wrong in most of the world. Use the platform formatter (`NumberFormat`/`DateTimeFormatter`, `Intl.*`, `NumberFormat`/`DateFormat` in Dart's `intl`, `FormatStyle` in Swift). Currency in particular: the symbol, its position, the decimal separator and the number of decimal places are all per-locale, and the currency itself is a property of the *price*, not of the user.
|
|
26
|
+
|
|
27
|
+
**Device locale, app language, and region are three things.** A user can read Portuguese, live in Japan and want metric units. Recent platforms support a per-app language setting (Android's per-app language preferences, iOS's app language in Settings) — support it rather than reading only the device's first locale, and never infer language from the country or from a SIM.
|
|
28
|
+
|
|
29
|
+
## Right-to-left
|
|
30
|
+
|
|
31
|
+
Arabic, Hebrew, Persian and Urdu mirror the interface. Getting this right is mostly a matter of never expressing a direction as "left" or "right":
|
|
32
|
+
|
|
33
|
+
- Use **start/end** everywhere: `paddingStart`, `layoutDirection`-aware `EdgeInsets.directional`, `marginStart`, SwiftUI's automatic leading/trailing. A `paddingLeft` is a bug in RTL.
|
|
34
|
+
- **Icons that imply direction mirror; icons that depict objects do not.** A back arrow and a progress chevron mirror; a play button, a clock and a logo do not. Both platforms have a flag for automatic mirroring (`autoMirrored` in Android vector drawables and Material icons, `flipsForRightToLeftLayoutDirection` on iOS images, `matchTextDirection` in Flutter); set it deliberately per icon.
|
|
35
|
+
- **Numbers and embedded Latin text inside RTL** follow the bidirectional algorithm and can render in a surprising order. Test with real strings containing a phone number, a price and a URL.
|
|
36
|
+
- Test it without waiting for a translation: Android's "Force RTL layout direction" developer option, Xcode's right-to-left pseudolanguage, and Flutter's `Directionality` widget in a test all show the layout before any Arabic exists.
|
|
37
|
+
|
|
38
|
+
## Layout that survives translation
|
|
39
|
+
|
|
40
|
+
- **Assume text grows.** German, Finnish and Russian commonly run 30–50% longer than English, and short strings grow proportionally more. Buttons and tabs with fixed widths, and single-line labels with `maxLines: 1` and no ellipsis, are where this shows.
|
|
41
|
+
- **No fixed heights on text containers**, for the same reason as text scaling in [the accessibility checklist](../SKILL.md).
|
|
42
|
+
- **Do not put text into images.** It cannot be translated, it does not scale with the font size setting, and screen readers cannot read it.
|
|
43
|
+
- **Pseudolocalization** finds these before translation exists: both platforms can render the app in an accented, expanded pseudo-language that makes truncation and hard-coded strings obvious at a glance. Run it in the normal review pass.
|
|
44
|
+
|
|
45
|
+
## Giving translators what they need
|
|
46
|
+
|
|
47
|
+
A translator sees a key and a string, not the screen. Ambiguous strings become wrong translations:
|
|
48
|
+
|
|
49
|
+
- **Comments on every non-obvious string.** "Open" as a verb on a button and "Open" as a status adjective are different words in most languages, and the resource comment is the only way the translator knows which.
|
|
50
|
+
- **Character limits** where the UI constrains the text, declared with the string.
|
|
51
|
+
- **Stable keys** that describe the role (`checkout_confirm_button`), not the English text — renaming the English copy should not orphan the translation.
|
|
52
|
+
- **Keep the placeholders documented**: what `{count}` counts, what `{name}` names.
|
|
53
|
+
|
|
54
|
+
## Reviewing
|
|
55
|
+
|
|
56
|
+
- [ ] No user-visible string literals in UI code.
|
|
57
|
+
- [ ] No sentence assembled by concatenation or string interpolation of fragments.
|
|
58
|
+
- [ ] Plurals use the platform's plural resources, not `if (n == 1)`.
|
|
59
|
+
- [ ] Dates, numbers and currencies use a locale-aware formatter; no manual symbol or separator.
|
|
60
|
+
- [ ] Layout uses start/end, not left/right; directional icons are marked as mirrorable.
|
|
61
|
+
- [ ] No fixed widths or heights on text containers; no text baked into images.
|
|
62
|
+
- [ ] The per-app language setting is supported, and the app does not infer language from the region.
|
|
63
|
+
- [ ] Strings carry comments and, where relevant, length limits.
|
|
64
|
+
- [ ] The app has been seen in pseudolocale and in forced RTL, at the largest font size.
|
|
65
|
+
- [ ] Anything untranslated falls back to the base language rather than showing a key.
|
|
66
|
+
|
|
67
|
+
The last three are the ones that catch real defects. Everything else can pass review and still produce a broken screen in Arabic at 200% font size, which is why the check is visual.
|
|
68
|
+
|
|
69
|
+
## Official documentation
|
|
70
|
+
|
|
71
|
+
- Android localization: https://developer.android.com/guide/topics/resources/localization
|
|
72
|
+
- Android per-app language preferences: https://developer.android.com/guide/topics/resources/app-languages
|
|
73
|
+
- Android bidirectional text support: https://developer.android.com/training/basics/supporting-devices/languages
|
|
74
|
+
- Apple localization: https://developer.apple.com/documentation/xcode/localization
|
|
75
|
+
- Flutter internationalization: https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization
|
|
76
|
+
- React Native right-to-left and localization: https://reactnative.dev/blog/2016/08/19/right-to-left-support-for-react-native-apps
|
|
77
|
+
- Unicode CLDR plural rules: https://cldr.unicode.org/index/cldr-spec/plural-rules
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-observability
|
|
3
|
+
description: Designs and reviews production observability for mobile apps on every stack — Flutter, native Android, native iOS, React Native and Kotlin Multiplatform. Covers crash and error reporting with usable context, breadcrumbs, structured logs, traces and custom metrics, release and cohort segmentation, correlation with backend requests, alerting thresholds, sampling and cost, and what must never be collected. Use when production problems cannot be explained from the available data, when setting up crash reporting or telemetry, or when defining release health alerts.
|
|
4
|
+
intents: [observability]
|
|
5
|
+
stacks: ["*"]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Mobile Observability
|
|
9
|
+
|
|
10
|
+
What you can learn about the app after it has shipped, on every stack. This file holds the model, the segmentation rules and the privacy limits; each platform's SDKs, symbol upload and field-metric sources live in its reference.
|
|
11
|
+
|
|
12
|
+
## 1. Pick the reference
|
|
13
|
+
|
|
14
|
+
| Project | Read |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Flutter | [references/flutter.md](references/flutter.md) |
|
|
17
|
+
| Native Android | [references/android.md](references/android.md) |
|
|
18
|
+
| Native iOS | [references/ios.md](references/ios.md) |
|
|
19
|
+
| React Native or Expo | [references/react-native.md](references/react-native.md) |
|
|
20
|
+
| Kotlin Multiplatform | [references/kotlin-multiplatform.md](references/kotlin-multiplatform.md) |
|
|
21
|
+
|
|
22
|
+
Firebase Crashlytics, Analytics and Performance Monitoring setup per stack is in [mobile-firebase](../mobile-firebase/SKILL.md). Release gating on these signals is in [mobile-release](../mobile-release/SKILL.md); the lab counterpart of the performance metrics is in [mobile-performance](../mobile-performance/SKILL.md).
|
|
23
|
+
|
|
24
|
+
## 2. Mobile is not a server
|
|
25
|
+
|
|
26
|
+
Everything below follows from four differences, and ignoring them is what produces a telemetry setup that costs money and answers nothing:
|
|
27
|
+
|
|
28
|
+
1. **You cannot log in to the device.** There is no `kubectl logs`. If the data was not collected and sent before the user gave up, it does not exist.
|
|
29
|
+
2. **The old version never goes away.** A server rolls back in minutes; an app version lives on devices for months. Every signal must be sliced by app version, or a fixed bug and a live bug look identical in the aggregate.
|
|
30
|
+
3. **The device is the constraint.** Telemetry costs the user's battery, data plan and storage. A payload sent per event is a wakeup per event — see [background.md](../mobile-performance/references/background.md).
|
|
31
|
+
4. **Reports arrive late and out of order.** A crash is reported on the *next* launch; a device offline for a week uploads a week later. Never read the last hour of a metric as "what is happening now" without accounting for the reporting delay.
|
|
32
|
+
|
|
33
|
+
## 3. The signals, and what each one answers
|
|
34
|
+
|
|
35
|
+
| Signal | Answers | Usual failure |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| **Crashes** | The process died. Where, on which OS/device/version | Obfuscated stack traces — symbols not uploaded |
|
|
38
|
+
| **Non-fatal errors** | A caught failure the user saw as a broken screen | Everything caught and logged, so the important one is invisible |
|
|
39
|
+
| **ANRs / hangs / terminations** | The app froze or was killed, without a crash report | Only watching crashes and missing the majority of bad sessions |
|
|
40
|
+
| **Breadcrumbs** | What the user did in the seconds before the failure | Free text with no structure, useless for grouping |
|
|
41
|
+
| **Structured logs** | The detail behind one session | Sent for every session, at full volume, at full cost |
|
|
42
|
+
| **Traces / spans** | Where the time went in one flow, across app and backend | Instrumented only in the app, so the backend half is invisible |
|
|
43
|
+
| **Metrics** | The aggregate: latency percentiles, success rates, adoption | Averages instead of percentiles |
|
|
44
|
+
| **Product analytics** | Whether the user completed the journey at all | Treated as observability; it shows the drop, never the cause |
|
|
45
|
+
|
|
46
|
+
A production incident needs at least three of these to be explainable: **what broke** (crash/error), **what led to it** (breadcrumbs/logs), and **how many users** (metrics segmented by version).
|
|
47
|
+
|
|
48
|
+
## 4. Segmentation: the dimensions that must be on every signal
|
|
49
|
+
|
|
50
|
+
Attach these to every crash, error, span and metric, or the data cannot be sliced when it matters:
|
|
51
|
+
|
|
52
|
+
- **App version and build number** — non-negotiable; every comparison is version-over-version.
|
|
53
|
+
- **OS version and device model** — most "random" crashes are one OS version or one manufacturer.
|
|
54
|
+
- **Release channel / rollout stage** — internal, beta, staged rollout percentage, full.
|
|
55
|
+
- **A stable install id**, not a user id, for counting affected users. It must be resettable and must not follow the person across reinstalls or apps (see §8).
|
|
56
|
+
- **Locale and region** where formatting, right-to-left layout or regulation differ.
|
|
57
|
+
- **Connectivity class** at the time (offline, cellular, Wi-Fi) for anything network-related.
|
|
58
|
+
- **Feature flag / experiment state** for flags that change the code path. A crash present only in one variant is otherwise unattributable.
|
|
59
|
+
|
|
60
|
+
Keep the cardinality bounded. A dimension whose value is a user id, a timestamp or a free-text message turns a metric into millions of series and is the standard way to make an observability bill unpayable.
|
|
61
|
+
|
|
62
|
+
## 5. Error context: make one report enough
|
|
63
|
+
|
|
64
|
+
A stack trace alone rarely explains a mobile failure. A report is useful when it carries:
|
|
65
|
+
|
|
66
|
+
- **The stack trace, symbolicated.** Verify symbol upload in CI for every build variant — an unsymbolicated release build is the most common and most expensive gap. The reference names the mechanism per stack.
|
|
67
|
+
- **Custom keys** for the state that decides the code path: the screen, the entity id being acted on, the auth state, whether the outbox was non-empty, the flag values.
|
|
68
|
+
- **Breadcrumbs as structured events**, not sentences: `screen_view{name}`, `network{method, host, status, ms}`, `tap{target}`, `state_change{from, to}`. Cap them (the last 50–100) and drop the payloads.
|
|
69
|
+
- **The grouping key.** Reports group by stack signature; a generic wrapper (`throw AppException(e)`) collapses a hundred distinct causes into one issue and hides all of them. Preserve the cause chain, and put the variable detail in keys, not in the exception message.
|
|
70
|
+
- **A correlation id** shared with the backend (§6).
|
|
71
|
+
- **Whether the user saw it.** A caught-and-recovered error and a blank screen are different severities and should be distinguishable without reading code.
|
|
72
|
+
|
|
73
|
+
## 6. Correlation with the backend
|
|
74
|
+
|
|
75
|
+
Mobile latency is mostly backend latency, and the app's "the request was slow" is unactionable on its own.
|
|
76
|
+
|
|
77
|
+
- Generate a **request id** per outbound call on the device, send it in a header, log it on both sides, and put it on the client-side span and on any error report from that call. One id makes "slow for this user at that minute" a single backend query instead of a guess.
|
|
78
|
+
- Use **W3C Trace Context** (`traceparent`) when the backend is already traced, so the mobile span is the parent of the server spans rather than a disconnected record. OpenTelemetry clients exist for mobile; the reference names the current state per stack.
|
|
79
|
+
- Attach a **session id** that spans the app session, so several failing calls can be recognized as one bad session.
|
|
80
|
+
- **Propagate nothing sensitive** in these headers, and never put a token in a trace attribute.
|
|
81
|
+
|
|
82
|
+
Without correlation, the mobile team and the backend team argue from two datasets. With it, the argument is a join.
|
|
83
|
+
|
|
84
|
+
## 7. Alerting: on the few things that mean "stop the rollout"
|
|
85
|
+
|
|
86
|
+
Most mobile alerts are noise because they fire on a raw count that tracks daily usage. Alert on **rates**, segmented by version, with the reporting delay in mind:
|
|
87
|
+
|
|
88
|
+
| Alert | Reasonable shape |
|
|
89
|
+
|---|---|
|
|
90
|
+
| Crash-free sessions drop | Below your baseline (commonly 99.5%+ for a mature app) **for the new version**, compared against the previous one |
|
|
91
|
+
| ANR / hang rate | Above the platform's own bad-behaviour threshold; both stores act on this |
|
|
92
|
+
| New issue in a new release | Any issue whose first occurrence is this version and which affects more than N users — this is the one that stops a staged rollout |
|
|
93
|
+
| Critical journey success rate | Login, checkout, sync drain: success percentage per version, not per absolute count |
|
|
94
|
+
| Startup and key-screen latency | P90, per version, against the previous release's value |
|
|
95
|
+
|
|
96
|
+
Rules that keep alerts trustworthy: compare a version against the previous version and not against last week; require a minimum session count before a percentage can fire; suppress alerts during the first minutes of a rollout when the denominator is tiny; and route each alert to someone who can act — a page for release health, a dashboard for everything else. An alert nobody acts on should be deleted, not muted.
|
|
97
|
+
|
|
98
|
+
## 8. Sampling and cost
|
|
99
|
+
|
|
100
|
+
Full-fidelity telemetry from every session is neither affordable nor kind to the device.
|
|
101
|
+
|
|
102
|
+
- **Never sample crashes, ANRs or fatal errors.** They are rare and each one matters.
|
|
103
|
+
- **Sample traces and verbose logs** — a small percentage of sessions at full detail answers most questions. Make the decision **per session, not per event**, so a sampled session is complete and readable end to end.
|
|
104
|
+
- **Always keep the sessions that failed.** Sampling that discards the broken session is worse than no sampling: raise the rate for sessions with an error, or buffer locally and upload only on failure.
|
|
105
|
+
- **Batch uploads**, with a size and a time bound, and flush on background/terminate. One flush per event is a radio wakeup per event.
|
|
106
|
+
- **Bound the local buffer** and drop the oldest when it is full; an unbounded telemetry queue fills the device and turns a monitoring feature into a support ticket.
|
|
107
|
+
- **Respect the network.** Do not upload telemetry on metered or constrained connections unless it is a crash report.
|
|
108
|
+
- **Record the sampling rate with the data.** A percentile computed from a sampled set without knowing the rate is wrong, and someone will read it as absolute.
|
|
109
|
+
|
|
110
|
+
## 9. What must never be collected
|
|
111
|
+
|
|
112
|
+
Observability is where privacy violations happen by accident, because logging "the request" logs whatever was in it.
|
|
113
|
+
|
|
114
|
+
- **Never log:** passwords, tokens, refresh tokens, API keys, full payment data, government ids, health data, precise location, message or document contents, or full request/response bodies from authenticated endpoints.
|
|
115
|
+
- **Redact at the source**, in the SDK's callback before send (every reporter has one), not in the backend pipeline. Data that left the device is already disclosed.
|
|
116
|
+
- **URLs carry secrets** in query strings — strip query parameters, or allow-list them. A URL is an attribute, not a free-text field.
|
|
117
|
+
- **Crash reports include more than you sent**: register values, thread names, and sometimes fragments of memory in native crashes. Treat the whole report as sensitive and restrict who can read it.
|
|
118
|
+
- **Consent gates collection.** On both stores, telemetry and analytics must match the declared data-safety/privacy-nutrition disclosures and the app's consent state. A user who declined analytics must not have analytics sent, and the SDK must be initialized accordingly — not initialized and then asked to stay quiet.
|
|
119
|
+
- **Prefer aggregation and short retention** for anything user-linked, and be able to delete a person's data on request, which requires the install id to be mappable and resettable.
|
|
120
|
+
|
|
121
|
+
This overlaps with [mobile-security](../mobile-security/SKILL.md); when in doubt, the answer is to send less.
|
|
122
|
+
|
|
123
|
+
## 10. Reviewing an existing setup
|
|
124
|
+
|
|
125
|
+
- [ ] Symbol/mapping upload happens in CI for every shipped variant, and a release build's crash is verifiably readable.
|
|
126
|
+
- [ ] App version, OS, device, channel and flag state are on every signal.
|
|
127
|
+
- [ ] ANRs, hangs and background terminations are collected, not just crashes.
|
|
128
|
+
- [ ] Caught errors are distinguishable from user-visible failures, and grouping is not collapsed by a generic wrapper.
|
|
129
|
+
- [ ] Breadcrumbs are structured and capped; logs are sampled; crashes are not.
|
|
130
|
+
- [ ] A client-generated correlation id reaches the backend and appears in error reports.
|
|
131
|
+
- [ ] Alerts are rate-based, per version, with a minimum-volume guard, and each one has an owner.
|
|
132
|
+
- [ ] Telemetry uploads are batched, bounded, flushed on background, and off on metered connections.
|
|
133
|
+
- [ ] A redaction callback exists and is tested; no secrets, bodies or precise location are sent.
|
|
134
|
+
- [ ] Collection respects consent and matches the store privacy declarations.
|
|
135
|
+
- [ ] Someone can answer "how many users on 4.2.0 hit this, and what were they doing" without adding code.
|
|
136
|
+
|
|
137
|
+
The last line is the real test. If answering an ordinary production question requires shipping a new build and waiting two weeks for adoption, the setup has failed regardless of how many SDKs are installed.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Observability on native Android
|
|
2
|
+
|
|
3
|
+
Android is the only mobile platform that gives you meaningful field data **without installing anything**: Play Console's Android vitals reports crashes, ANRs, wakeups, wake locks and rendering for your installed base, sliced by version, device and OS. Start there, then add an SDK for the context vitals cannot give you.
|
|
4
|
+
|
|
5
|
+
## What you get for free
|
|
6
|
+
|
|
7
|
+
| Source | Gives |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Play Console → Android vitals | Crash rate, **ANR rate** and user-perceived ANR rate, excessive wakeups, stuck partial wake locks, slow/frozen frames, app size — all per version and per device model, with bad-behaviour thresholds the store itself acts on |
|
|
10
|
+
| `ApplicationExitInfo` | Why the process last died — crash, ANR, low memory, user request, excessive resource use — readable **on the next launch**, including the ANR trace |
|
|
11
|
+
| Play Console → Reach and devices | The device/OS distribution you actually ship to, which decides what a "rare" crash really costs |
|
|
12
|
+
|
|
13
|
+
`ApplicationExitInfo` is the cheapest large win in Android observability and is frequently missing: it is the only way to see background terminations and OOM kills, which produce no crash report at all.
|
|
14
|
+
|
|
15
|
+
```kotlin
|
|
16
|
+
val am = getSystemService(ActivityManager::class.java)
|
|
17
|
+
am.getHistoricalProcessExitReasons(packageName, /* pid = */ 0, /* maxNum = */ 5)
|
|
18
|
+
.firstOrNull()
|
|
19
|
+
?.let { info ->
|
|
20
|
+
crashReporter.log(
|
|
21
|
+
"last_exit reason=${info.reason} status=${info.status} " +
|
|
22
|
+
"importance=${info.importance} description=${info.description}",
|
|
23
|
+
)
|
|
24
|
+
// REASON_ANR carries the ANR trace in info.traceInputStream
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Report it on the next launch, attached to the session, so an OOM kill becomes a searchable event rather than an unexplained gap.
|
|
29
|
+
|
|
30
|
+
## Crash and error reporting
|
|
31
|
+
|
|
32
|
+
Crashlytics (see [mobile-firebase](../../mobile-firebase/SKILL.md)), Sentry, Bugsnag and Embrace all cover JVM crashes, native (NDK) crashes and non-fatals. What matters is the configuration, not the vendor:
|
|
33
|
+
|
|
34
|
+
- **Mapping file upload for every release variant.** With R8/Proguard enabled, an un-uploaded mapping file produces an unreadable stack trace. The Crashlytics Gradle plugin uploads automatically when `mappingFileUploadEnabled` is on; other SDKs have a Gradle task. Verify it in CI by failing the build when the upload task did not run, and prove it once by symbolicating a deliberate release crash.
|
|
35
|
+
- **Native symbols** need a separate upload (`firebaseCrashlytics { nativeSymbolUploadEnabled = true }`, or the vendor's equivalent) or every NDK/Kotlin-Native frame is an address.
|
|
36
|
+
- **Custom keys** on every session: screen, auth state, feature-flag values, outbox depth, connectivity class.
|
|
37
|
+
- **Non-fatals** for caught failures that the user saw: `FirebaseCrashlytics.getInstance().recordException(e)` — and *not* for every caught exception, or the important one disappears.
|
|
38
|
+
- **Preserve the cause chain.** `throw AppException("failed", cause = e)` keeps grouping meaningful; `throw AppException("failed")` collapses every cause into one issue.
|
|
39
|
+
|
|
40
|
+
## ANRs and hangs
|
|
41
|
+
|
|
42
|
+
ANRs are the signal Android developers most often miss, because they produce no crash callback.
|
|
43
|
+
|
|
44
|
+
- Play Console's ANR rate is authoritative and is a store policy threshold.
|
|
45
|
+
- `ApplicationExitInfo` with `REASON_ANR` gives the trace on the device.
|
|
46
|
+
- Crash reporters detect ANRs with their own watchdogs; treat their counts as an early indicator and Play Console as the truth.
|
|
47
|
+
- Diagnose the cause with a system trace and the main-thread stack — see [mobile-performance/references/android.md](../../mobile-performance/references/android.md) and the ANR guide in [mobile-debug](../../mobile-debug/SKILL.md).
|
|
48
|
+
|
|
49
|
+
## Logs, traces and metrics
|
|
50
|
+
|
|
51
|
+
- **Structured logs.** `Log.d` goes nowhere in production. Use the reporter's breadcrumb API, or a `Timber`-style tree that forwards structured events to the reporter, and keep the tags bounded.
|
|
52
|
+
- **Traces.** Firebase Performance Monitoring gives automatic app-start, screen-rendering and HTTP traces plus `Trace` for custom spans, with no backend work. OpenTelemetry's Android instrumentation is the option when you already run an OTel backend and want `traceparent` propagation into the server traces; check the current stability of the artifacts before adopting them.
|
|
53
|
+
- **Custom metrics.** Prefer a small number of counters and durations with bounded attributes (version, screen, result) over free-form events.
|
|
54
|
+
- **Network correlation.** An OkHttp `Interceptor` is the natural place to generate and attach the request id, record the span, and add a breadcrumb — one implementation covers Retrofit, Coil and anything else on the same client.
|
|
55
|
+
|
|
56
|
+
```kotlin
|
|
57
|
+
class ObservabilityInterceptor(private val reporter: Reporter) : Interceptor {
|
|
58
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
59
|
+
val requestId = UUID.randomUUID().toString()
|
|
60
|
+
val request = chain.request().newBuilder().header("X-Request-Id", requestId).build()
|
|
61
|
+
val start = SystemClock.elapsedRealtime()
|
|
62
|
+
return try {
|
|
63
|
+
chain.proceed(request).also { response ->
|
|
64
|
+
reporter.breadcrumb(
|
|
65
|
+
"network",
|
|
66
|
+
mapOf(
|
|
67
|
+
"method" to request.method,
|
|
68
|
+
"host" to request.url.host, // host, not the full URL
|
|
69
|
+
"status" to response.code,
|
|
70
|
+
"ms" to SystemClock.elapsedRealtime() - start,
|
|
71
|
+
"request_id" to requestId,
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
} catch (e: IOException) {
|
|
76
|
+
reporter.breadcrumb("network_error", mapOf("host" to request.url.host, "request_id" to requestId))
|
|
77
|
+
throw e
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Note what is absent: no full URL (query strings carry tokens), no headers, no bodies.
|
|
84
|
+
|
|
85
|
+
## Privacy and consent
|
|
86
|
+
|
|
87
|
+
- Play requires an accurate **Data safety** declaration covering everything the SDKs collect, including the ones you did not configure yourself. Audit the merged manifest and the dependency list, not just your own code.
|
|
88
|
+
- Gate initialization on consent. Analytics collection can be disabled by default in the manifest and enabled after consent (`firebase_analytics_collection_enabled`, `firebase_crashlytics_collection_enabled` and the equivalent runtime setters) — initializing and then hoping the SDK stays quiet is not the same thing.
|
|
89
|
+
- Use a resettable id (the reporter's install id, or `AdvertisingIdClient` only if advertising is genuinely the purpose and it is declared). `ANDROID_ID` and any hardware identifier are not acceptable install ids.
|
|
90
|
+
- Register a redaction callback / `beforeSend` equivalent and unit-test it against a payload containing a token.
|
|
91
|
+
|
|
92
|
+
## CI
|
|
93
|
+
|
|
94
|
+
- Fail the build when the mapping or native-symbol upload task did not run for a release variant.
|
|
95
|
+
- Post the build's version name and code into the reporter so a release appears in the dashboard the moment it ships.
|
|
96
|
+
- Check Android vitals as part of the staged-rollout gate — see [mobile-release](../../mobile-release/SKILL.md).
|
|
97
|
+
|
|
98
|
+
## Official documentation
|
|
99
|
+
|
|
100
|
+
- Android vitals: https://developer.android.com/topic/performance/vitals
|
|
101
|
+
- ApplicationExitInfo: https://developer.android.com/reference/android/app/ApplicationExitInfo
|
|
102
|
+
- ANRs: https://developer.android.com/topic/performance/vitals/anr
|
|
103
|
+
- Crashlytics for Android: https://firebase.google.com/docs/crashlytics/get-started?platform=android
|
|
104
|
+
- Firebase Performance Monitoring: https://firebase.google.com/docs/perf-mon/get-started-android
|
|
105
|
+
- Play Data safety: https://support.google.com/googleplay/android-developer/answer/10787469
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Observability in Flutter
|
|
2
|
+
|
|
3
|
+
Flutter's specific problem is that a failure can happen in three places with three different capture mechanisms: **Dart** (uncaught exceptions in the framework, in your code, or in an isolate), the **Flutter engine/native layer** (a real native crash), and the **platform channels** between them. A setup that only wires `FlutterError.onError` misses two of the three.
|
|
4
|
+
|
|
5
|
+
## Capture all three layers
|
|
6
|
+
|
|
7
|
+
```dart
|
|
8
|
+
Future<void> main() async {
|
|
9
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
10
|
+
await Firebase.initializeApp();
|
|
11
|
+
|
|
12
|
+
// 1. Errors the Flutter framework catches (build, layout, paint, gestures).
|
|
13
|
+
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
|
|
14
|
+
|
|
15
|
+
// 2. Errors from the Dart zone that the framework does not catch:
|
|
16
|
+
// async gaps, futures without a catch, platform-channel replies.
|
|
17
|
+
PlatformDispatcher.instance.onError = (error, stack) {
|
|
18
|
+
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
runApp(const App());
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `FlutterError.onError` alone leaves every unhandled async error unreported.
|
|
27
|
+
- A background `Isolate` has its own error handling: add an `Isolate.current.addErrorListener` (or the reporter's helper) inside the isolate entry point, or its crashes are silent.
|
|
28
|
+
- Native crashes in the engine or in a plugin's Android/iOS code are captured by the native SDK the Flutter package installs — which is why the plugin's native setup steps matter, not just the Dart ones.
|
|
29
|
+
|
|
30
|
+
Sentry, Bugsnag, Embrace and Crashlytics all follow this shape; the reference for the Firebase path is [mobile-firebase](../../mobile-firebase/SKILL.md).
|
|
31
|
+
|
|
32
|
+
## Symbols
|
|
33
|
+
|
|
34
|
+
A release Flutter build is AOT-compiled, and its Dart stack traces are **obfuscated addresses** unless the debug info is uploaded.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
flutter build appbundle --obfuscate --split-debug-info=build/symbols
|
|
38
|
+
flutter build ipa --obfuscate --split-debug-info=build/symbols
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then upload `build/symbols` with the vendor's command (`firebase crashlytics:symbols:upload`, `sentry-cli debug-files upload`, …) **for every build you ship**, and keep the directory per version — symbols from a different build do not resolve. On Android you still need the R8 mapping file for the Kotlin side, and on iOS the dSYMs for the native side: three uploads, not one.
|
|
42
|
+
|
|
43
|
+
The most common production gap in Flutter apps is exactly this: Dart symbols uploaded during a manual first setup, never wired into CI, and unreadable stack traces from then on. Verify by crashing a release build on purpose once.
|
|
44
|
+
|
|
45
|
+
## Context and breadcrumbs
|
|
46
|
+
|
|
47
|
+
- **Navigation breadcrumbs** come free from a `NavigatorObserver`:
|
|
48
|
+
|
|
49
|
+
```dart
|
|
50
|
+
class ObservabilityObserver extends NavigatorObserver {
|
|
51
|
+
@override
|
|
52
|
+
void didPush(Route route, Route? previous) =>
|
|
53
|
+
reporter.breadcrumb('screen_view', {'name': route.settings.name ?? 'unnamed'});
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Route names must be set for this to be useful — an app built entirely from anonymous `MaterialPageRoute`s produces `unnamed` breadcrumbs.
|
|
58
|
+
|
|
59
|
+
- **Custom keys** per session: `setCustomKey` for the screen, auth state, flag values, outbox depth, connectivity.
|
|
60
|
+
- **Network breadcrumbs** belong in a Dio `Interceptor` or an `http` `BaseClient` wrapper, where the request id is also generated and attached. Record method, host, status and duration — never the full URL or the body.
|
|
61
|
+
- **State-management errors:** `Bloc.observer` (`onError`), Riverpod's `ProviderObserver`, or the equivalent hook forwards state-layer failures with the state that caused them, which a bare stack trace does not carry.
|
|
62
|
+
- Keep the cause chain: `Error.throwWithStackTrace(AppException(e), stack)` preserves the original trace, while `throw AppException('failed')` in a `catch` discards the only useful part of the report.
|
|
63
|
+
|
|
64
|
+
## Performance signals
|
|
65
|
+
|
|
66
|
+
- **Firebase Performance Monitoring** for Flutter gives app-start and custom traces; HTTP traces are collected automatically on the native side, and Dart-side calls need explicit `HttpMetric` instrumentation.
|
|
67
|
+
- The lab counterparts — frame timings, startup traces, `integration_test` timeline summaries — are in [mobile-performance/references/flutter.md](../../mobile-performance/references/flutter.md). Use the same metric names in both so a CI regression and a field regression are comparable.
|
|
68
|
+
- `SchedulerBinding.instance.addTimingsCallback` exposes real frame timings in a release build; sampled and aggregated, it is a usable field jank metric when a vendor SDK does not provide one.
|
|
69
|
+
|
|
70
|
+
## Platform field data still applies
|
|
71
|
+
|
|
72
|
+
Flutter does not replace Android vitals, Play Console ANR data, Xcode Organizer or MetricKit — the app is still an Android and an iOS process. ANRs and watchdog terminations in a Flutter app show up there and nowhere in the Dart layer. Read [android.md](android.md) and [ios.md](ios.md) alongside this file.
|
|
73
|
+
|
|
74
|
+
## Privacy and consent
|
|
75
|
+
|
|
76
|
+
- Both stores' declarations (Data safety, privacy nutrition label and `PrivacyInfo.xcprivacy`) must cover what the Flutter plugins collect natively, not just what your Dart code sends.
|
|
77
|
+
- Gate collection: `FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(false)` until consent, or the vendor's equivalent, set **before** the reporter would send anything.
|
|
78
|
+
- Implement the SDK's redaction hook (`beforeSend` in Sentry, a record filter elsewhere) and unit-test it with a payload containing a token.
|
|
79
|
+
|
|
80
|
+
## Official documentation
|
|
81
|
+
|
|
82
|
+
- Handling errors in Flutter: https://docs.flutter.dev/testing/errors
|
|
83
|
+
- Obfuscating Dart code: https://docs.flutter.dev/deployment/obfuscate
|
|
84
|
+
- Crashlytics for Flutter: https://firebase.google.com/docs/crashlytics/get-started?platform=flutter
|
|
85
|
+
- Firebase Performance Monitoring for Flutter: https://firebase.google.com/docs/perf-mon/flutter/get-started
|
|
86
|
+
- Flutter DevTools: https://docs.flutter.dev/tools/devtools/overview
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Observability on native iOS
|
|
2
|
+
|
|
3
|
+
Apple's own telemetry is good and under-used: **MetricKit** delivers per-device diagnostics and metrics to your app, and **Xcode Organizer** aggregates crashes, hangs, launch time, memory, disk writes and battery per release from users who opted into sharing. Both are free, privacy-reviewed and need no SDK. Add a third-party reporter for the context they do not carry — breadcrumbs, custom keys, correlation ids.
|
|
4
|
+
|
|
5
|
+
## What you get from Apple
|
|
6
|
+
|
|
7
|
+
| Source | Gives |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Xcode Organizer → Crashes | Symbolicated crash reports per version, grouped, from opted-in users |
|
|
10
|
+
| Xcode Organizer → Hangs, Launches, Memory, Disk Writes, Battery | Field metrics per version, with a comparison to the previous release |
|
|
11
|
+
| MetricKit (`MXMetricManager`) | Daily metric payloads (`MXAppLaunchMetric`, `MXAnimationMetric`, `MXAppExitMetric`, `MXSignpostMetric`, and more) and diagnostic payloads (crash, hang, disk-write, CPU exception) delivered **into your app**, so you can forward them to your own backend |
|
|
12
|
+
|
|
13
|
+
`MXAppExitMetric` is the one to wire up first: it reports how sessions ended, including **memory-pressure terminations and watchdog kills**, which produce no crash report and are otherwise invisible.
|
|
14
|
+
|
|
15
|
+
```swift
|
|
16
|
+
final class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
|
|
17
|
+
func didReceive(_ payloads: [MXMetricPayload]) {
|
|
18
|
+
for payload in payloads {
|
|
19
|
+
backend.upload(payload.jsonRepresentation(), kind: "metric")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func didReceive(_ payloads: [MXDiagnosticPayload]) {
|
|
24
|
+
for payload in payloads {
|
|
25
|
+
backend.upload(payload.jsonRepresentation(), kind: "diagnostic") // hangs and crashes
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// In application(_:didFinishLaunchingWithOptions:)
|
|
31
|
+
MXMetricManager.shared.add(subscriber)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Payloads arrive **once a day, about the previous day** — plan the dashboard around that delay rather than expecting live data.
|
|
35
|
+
|
|
36
|
+
## Crash and error reporting
|
|
37
|
+
|
|
38
|
+
- **dSYMs must reach the reporter for every build.** With bitcode gone this is a straightforward upload, but it still breaks constantly: Xcode Cloud, a CI cache, or `DEBUG_INFORMATION_FORMAT` set to `dwarf` instead of `dwarf-with-dsym` will all produce unreadable release crashes. Run the vendor's upload script as a build phase *and* verify in CI, then prove it once with a deliberate release crash.
|
|
39
|
+
- **Swift errors are not exceptions.** A crash reporter sees `fatalError`, force-unwraps, array bounds, and signals — not a `throw` you caught. User-visible failures from `catch` blocks must be reported explicitly as non-fatals, with the state that explains them.
|
|
40
|
+
- **Custom keys** per session: scene/screen, auth state, flag values, outbox depth, connectivity (`NWPathMonitor`'s current path).
|
|
41
|
+
- **Grouping** follows the stack signature. A generic `AppError.wrapped(Error)` rethrown from one place collapses unrelated failures into one issue; keep the original error type and domain in the report.
|
|
42
|
+
|
|
43
|
+
## Hangs
|
|
44
|
+
|
|
45
|
+
Apple treats the main thread being unresponsive beyond roughly 250 ms as a hang, and Organizer reports a hang rate per hour of use.
|
|
46
|
+
|
|
47
|
+
- Organizer → Hangs and MetricKit's hang diagnostics give the field data with a stack.
|
|
48
|
+
- The Thread Performance Checker (a debug-time diagnostic) catches main-thread file I/O and priority inversions before they ship.
|
|
49
|
+
- Diagnose with Instruments — see [mobile-performance/references/ios.md](../../mobile-performance/references/ios.md).
|
|
50
|
+
|
|
51
|
+
Watchdog terminations (`0x8badf00d`) are hangs that lasted long enough to be killed; they appear in `MXAppExitMetric` and in Organizer, not as ordinary crashes.
|
|
52
|
+
|
|
53
|
+
## Logs, traces and metrics
|
|
54
|
+
|
|
55
|
+
- **`OSLog` / `Logger` is the system logger**: structured, privacy-aware, and cheap. It does **not** leave the device — treat it as what you read on a connected device or in a sysdiagnose, and forward what you need to your reporter separately.
|
|
56
|
+
- Mark values explicitly: `logger.info("order \(id, privacy: .private) failed with \(code, privacy: .public)")`. Dynamic strings default to private and are redacted in collected logs — deliberately marking a secret `.public` is the mistake to look for in review.
|
|
57
|
+
- **Signposts** (`OSSignposter`) mark intervals that Instruments can read *and* that MetricKit aggregates as `MXSignpostMetric` from the field — the same instrumentation serves the lab and production.
|
|
58
|
+
- **Network correlation:** a `URLProtocol`, a custom `URLSessionTaskDelegate`, or a thin client wrapper generates the request id, adds the header, records the duration and drops a breadcrumb with **host and status only** — never the full URL or the body.
|
|
59
|
+
|
|
60
|
+
```swift
|
|
61
|
+
var request = URLRequest(url: url)
|
|
62
|
+
let requestID = UUID().uuidString
|
|
63
|
+
request.setValue(requestID, forHTTPHeaderField: "X-Request-Id")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **OpenTelemetry** has a Swift implementation for teams already running an OTel backend and wanting `traceparent` propagation into server traces; check the current maturity of the package before adopting it.
|
|
67
|
+
|
|
68
|
+
## Privacy and consent
|
|
69
|
+
|
|
70
|
+
- The **privacy nutrition label** and the **privacy manifest** (`PrivacyInfo.xcprivacy`) must cover everything collected, including by third-party SDKs — and SDKs on Apple's required-reason list must ship their own manifest and signature. A reporter added for observability changes your declared data collection.
|
|
71
|
+
- **App Tracking Transparency** applies to tracking across apps and websites, not to first-party crash reporting; do not conflate them, but do check what your SDK's default configuration sends.
|
|
72
|
+
- Gate SDK initialization on consent where consent is required, and use the reporter's own resettable install id rather than `identifierForVendor` if the id must be resettable independently of app removal.
|
|
73
|
+
- Implement the vendor's `beforeSend` callback and test it against a payload containing a token.
|
|
74
|
+
|
|
75
|
+
## CI
|
|
76
|
+
|
|
77
|
+
- Fail the archive job when dSYM upload did not run, and when `DEBUG_INFORMATION_FORMAT` is not `dwarf-with-dsym` for Release.
|
|
78
|
+
- Upload the build's version and build number to the reporter so the release appears immediately.
|
|
79
|
+
- Check Organizer's hang rate and crash rate for the previous release before promoting a new one — see [mobile-release](../../mobile-release/SKILL.md).
|
|
80
|
+
|
|
81
|
+
## Official documentation
|
|
82
|
+
|
|
83
|
+
- MetricKit: https://developer.apple.com/documentation/metrickit
|
|
84
|
+
- Diagnosing issues using crash reports and device logs: https://developer.apple.com/documentation/xcode/diagnosing-issues-using-crash-reports-and-device-logs
|
|
85
|
+
- Understanding hangs in your app: https://developer.apple.com/documentation/xcode/understanding-hangs-in-your-app
|
|
86
|
+
- Logging with OSLog: https://developer.apple.com/documentation/os/logging
|
|
87
|
+
- Recording performance data with signposts: https://developer.apple.com/documentation/os/recording-performance-data
|
|
88
|
+
- Privacy manifest files: https://developer.apple.com/documentation/bundleresources/privacy-manifest-files
|