iOS/iPadOS device vitals: skip no-op writes to reduce writer load - #50581
iOS/iPadOS device vitals: skip no-op writes to reduce writer load#50581nulmete wants to merge 9 commits into
Conversation
…50046) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Relates to #49984 This is PR 1 of 2 for #49984, split for ease of review and stacked onto a feature branch (`49984-ios-ipados-vitals`). This PR adds the MySQL storage (`host_mdm_apple_device_vitals`, `host_mdm_apple_service_subscriptions`) for the 29 additional iOS/iPadOS vitals, and expands the `DeviceInformation` MDM command's `Queries` array to request the corresponding Apple keys. It does not yet parse or persist any new ack data — that's PR 2 (#50047), stacked on top of this branch. # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. It will be included in the feature branch. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually (see results on PR2) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for collecting and storing Apple MDM device vitals and cellular service subscription details. * Expanded Apple device information requests to include the full set of supported query fields. * Added database support for retaining device vitals and subscription data. * **Bug Fixes** * Host deletion now also removes associated Apple MDM vitals and subscription records. * **Tests** * Added coverage for vitals updates, nullable values, subscription synchronization, database constraints, and device information requests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Relates to #49984 This is PR 2 of 2 for #49984, stacked on top of PR 1 (#50046) — diff here is scoped to just the ack-parsing/wiring work on top of that PR's storage and command changes. This PR parses the additional `DeviceInformation` ack fields requested by PR 1's command expansion and persists them via `SetOrUpdateHostMDMAppleDeviceVitals`. A persistence failure is logged rather than aborting the MDM check-in, since it's a non-critical write the next refetch will redo. # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. It will be included in the feature branch. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually Enrolled my iPhone to Fleet (dual-SIM, physical + eSIM): <img width="1458" height="830" alt="Screenshot 2026-07-28 at 10 57 00 AM" src="https://github.com/user-attachments/assets/1735b5e6-ac2d-4dc1-a66b-c8d8063b7c76" /> `host_mdm_apple_device_vitals` — populated as expected: - `battery_level: 0.87`, `cellular_technology: 1`, all `is_*` booleans, `last_cloud_backup_date` — parsed correctly - `device_properties_attestation`: JSON array of **2** base64 DER certs (leaf + "Apple Enterprise Attestation Sub CA" intermediate) — confirms it's a cert chain, not a boolean, per the deviation noted in the parent issue - `mdm_options: {}` — key present, no sub-options applicable → empty object, not `NULL` (expected `omitempty` behavior) - `push_token: NULL` — expected; Apple only returns this for user-channel enrollments, not device-channel - `organization_info` / `accessibility_settings: NULL` — expected, nothing configured/toggled - `model_number`, `modem_firmware_version`, `supplemental_build_version`, `bluetooth_mac`, `wifi_mac`, `eas_device_identifier`, `itunes_store_account_hash` — all populated `host_mdm_apple_service_subscriptions` — **2 rows** for one dual-SIM device, confirming the multi-slot replace logic works on real hardware: - `CTSubscriptionSlotOne` — fully populated (carrier, MCC/MNC, ICCID, phone number, etc.) - `CTSubscriptionSlotTwo` — only `eid`/`imei` populated, everything else `NULL` — an inactive/unprovisioned eSIM slot reporting a sparse row, exactly the shape the tests were built to mirror <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Apple device management now captures and stores additional iPhone and iPad device vitals during device information refreshes. * Supports details such as Wi‑Fi MAC address, lost mode status, operating system information, accessibility settings, organization details, and service subscriptions. * **Bug Fixes** * Missing or unexpected device information no longer interrupts refresh processing. * Failures saving supplementary device vitals no longer prevent other device management actions from completing. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #49985 Exposes the the 29 iOS/iPadOS device vitals fields in `GET /hosts/:id` and `GET /hosts/identifier/:identifier` for iOS/iPadOS hosts. # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. Will be added in feature branch. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Host details for iOS and iPadOS devices now include available Apple MDM device vitals and service subscription information. * Vitals are shown only when present, while unsupported platforms and missing data remain omitted. * Added support for nested accessibility, organization, MDM options, and attestation details in host responses. * **Bug Fixes** * Improved handling of hosts without stored Apple MDM vitals, avoiding errors and incomplete responses. * **Tests** * Added coverage for complete, partial, and missing vitals data across host detail endpoints. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #49986 Adds a "View all" button to the Vitals card for iOS/iPadOS hosts, opening a new modal that lists the 29 new device vitals alphabetically. `View all` button is only shown for iOS/iPadOS hosts, and the vitals card is capped to two full rows. Clicking `View all` shows all the vitals, including the new 29 apple device vitals. See Figma: https://www.figma.com/design/x5pERdbyiHYv0U86CfJCpd/-39281-iOS-iPadOS--More-host-vitals?node-id=5598-359&t=FUpSwSHydm81VMgg-0 `Service subscriptions` vitals are rendered as `Service subscription 1`, `Service subscription 2`, and so on. Hovering over each of them shows all the key-value pairs for that subscription. <img width="1778" height="970" alt="Screenshot 2026-08-03 at 8 41 23 AM" src="https://github.com/user-attachments/assets/3d212d0c-2e85-4715-8bb1-92db8520d8a2" /> NOTE: the Figma/spec suggested to add a `Not supported` value + tooltip for any vital that is not supported for a particular platform/enrollment type, but I couldn't notice any after the manual tests that were run on this branch (my own iPhone, and Magnus/Rachel's iPad/iPhones). If during QA we detect some fields not being supported for certain enrollments, I can definitely add that code back. # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. Will be added in the feature branch. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <img width="828" height="1340" alt="Screenshot 2026-08-03 at 8 45 01 AM" src="https://github.com/user-attachments/assets/912c21dc-47fc-4989-b93f-2777fdc993b4" /> <img width="836" height="1164" alt="Screenshot 2026-08-03 at 8 46 05 AM" src="https://github.com/user-attachments/assets/ecbe33b1-81c5-4fb9-9ee3-826329ad8dfa" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a “View all” option for iOS and iPadOS host vitals, opening a dedicated Vitals modal. * The Vitals modal presents a complete, alphabetically sorted vitals list with improved formatting for booleans, percentages, dates, nested values, and subscription entries. * **Bug Fixes** * Updated vitals behavior to correctly hide Location when applicable (including ADE mode rules). * Improved handling for unsupported/unavailable vitals by showing clear “Not supported” messaging and tooltips. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
# Conflicts: # server/datastore/mysql/schema.sql
Four migrations landed on main after this one was written, so 20260727191311 would have sorted ahead of them. Retimestamped to 20260804143233 and regenerated schema.sql.
…#50514) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Relates to #49984 Restricts device vitals fields added by #49984 (battery level, service subscriptions/phone numbers, accessibility settings, etc.) to non-BYOD iOS/iPadOS enrollments only. Personal (BYOD) hosts continue to get the 9 fields Fleet requested before #49984 (device name, capacity, OS version, product name, lost-mode status, etc.), but not the newer ones, since those can carry PII a personal device's owner may not want their employer to see. Note: Raised during review of #50046, see [this comment](#50046 (review)). ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved support for personally enrolled iOS and iPadOS devices. * Personally enrolled devices now receive a streamlined device-information request. * Vitals for personally enrolled devices display the full list without a “View all” limit or button. * **Bug Fixes** * Corrected Vitals card behavior for BYOD devices while preserving existing behavior for other hosts. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
SetOrUpdateHostMDMAppleDeviceVitals now compares against what's already stored and skips the UPDATE when nothing changed, same as SetOrUpdateHostDisksSpace/UpdateHostOperatingSystem already do for other per-refetch host data. Applies to both host_mdm_apple_device_vitals and host_mdm_apple_service_subscriptions. Battery level tolerates a 5-point delta so it doesn't defeat the optimization on every refetch.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
WalkthroughApple MDM device vitals now use no-op detection before persistence. Comparisons tolerate small battery changes, equivalent timestamps, and reordered JSON keys. Changed or missing rows retain update-then-insert behavior. Subscription replacement loads existing records, removes stale slots, and skips unchanged records. Tests cover helper comparisons and end-to-end persistence behavior. Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@server/datastore/mysql/apple_mdm_device_vitals.go`:
- Around line 164-176: Update batteryLevelPtrEqual so an exact
five-percentage-point change, including 0.40 versus 0.45, is treated as changed
despite floating-point rounding; use a small boundary epsilon or quantized
battery-point comparison while preserving nil handling and the under-five-point
equality behavior. Add a regression test covering 0.40 and 0.45.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a5047e10-b63a-4c81-9a4f-cd514be7dd91
📒 Files selected for processing (2)
server/datastore/mysql/apple_mdm_device_vitals.goserver/datastore/mysql/apple_mdm_device_vitals_test.go
| // batteryLevelPtrEqual treats a battery level change under 5 percentage | ||
| // points as unchanged, since it otherwise drifts on nearly every refetch and | ||
| // would defeat the point of skipping no-op writes. | ||
| func batteryLevelPtrEqual(a, b *float64) bool { | ||
| const batteryLevelTolerance = 0.05 | ||
| if a == nil || b == nil { | ||
| return a == b | ||
| } | ||
| diff := *a - *b | ||
| if diff < 0 { | ||
| diff = -diff | ||
| } | ||
| return diff < batteryLevelTolerance |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle floating-point rounding at the battery threshold.
Line 176 compares a binary floating-point difference directly with 0.05. For example, 0.45 - 0.40 can evaluate slightly below 0.05. An exact five-point change can then skip persistence.
Compare with a small boundary epsilon, or compare quantized battery points. Add a regression case for 0.40 and 0.45.
Proposed fix
func batteryLevelPtrEqual(a, b *float64) bool {
const batteryLevelTolerance = 0.05
+ const batteryLevelBoundaryEpsilon = 1e-9
if a == nil || b == nil {
return a == b
}
diff := *a - *b
if diff < 0 {
diff = -diff
}
- return diff < batteryLevelTolerance
+ return diff < batteryLevelTolerance-batteryLevelBoundaryEpsilon
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // batteryLevelPtrEqual treats a battery level change under 5 percentage | |
| // points as unchanged, since it otherwise drifts on nearly every refetch and | |
| // would defeat the point of skipping no-op writes. | |
| func batteryLevelPtrEqual(a, b *float64) bool { | |
| const batteryLevelTolerance = 0.05 | |
| if a == nil || b == nil { | |
| return a == b | |
| } | |
| diff := *a - *b | |
| if diff < 0 { | |
| diff = -diff | |
| } | |
| return diff < batteryLevelTolerance | |
| // batteryLevelPtrEqual treats a battery level change under 5 percentage | |
| // points as unchanged, since it otherwise drifts on nearly every refetch and | |
| // would defeat the point of skipping no-op writes. | |
| func batteryLevelPtrEqual(a, b *float64) bool { | |
| const batteryLevelTolerance = 0.05 | |
| const batteryLevelBoundaryEpsilon = 1e-9 | |
| if a == nil || b == nil { | |
| return a == b | |
| } | |
| diff := *a - *b | |
| if diff < 0 { | |
| diff = -diff | |
| } | |
| return diff < batteryLevelTolerance-batteryLevelBoundaryEpsilon | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/datastore/mysql/apple_mdm_device_vitals.go` around lines 164 - 176,
Update batteryLevelPtrEqual so an exact five-percentage-point change, including
0.40 versus 0.45, is treated as changed despite floating-point rounding; use a
small boundary epsilon or quantized battery-point comparison while preserving
nil handling and the under-five-point equality behavior. Add a regression test
covering 0.40 and 0.45.
There was a problem hiding this comment.
Good catch — 0.45 - 0.40 evaluates to 0.04999999999999999 in float64, so an exact 5-point change could round the wrong way and get skipped. Fixed by subtracting a small epsilon from the tolerance boundary, plus a regression test for 0.40 vs 0.45: 5d137dd.
diff < 0.05 could misclassify an exact 5-point delta (e.g. 0.40 vs 0.45) as unchanged, since that subtraction rounds to slightly under 0.05 in binary floating point. Subtract a tiny epsilon from the boundary so exact-5-point deltas are consistently treated as changed.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 49984-ios-ipados-vitals #50581 +/- ##
===========================================================
- Coverage 68.28% 68.27% -0.01%
===========================================================
Files 3953 3953
Lines 252500 252492 -8
Branches 13495 13495
===========================================================
- Hits 172410 172392 -18
- Misses 64681 64687 +6
- Partials 15409 15413 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Related issue: Relates to #49984
Every
DeviceInformationrefetch unconditionally rewrote both new vitals tables even when nothing had changed since the last refetch — raised as a nit during review of #50046 (#50046 (comment)). This mirrors whatSetOrUpdateHostDisksSpace/UpdateHostOperatingSystemalready do for other per-refetch host data: skip the write when the stored data already matches. Battery level tolerates a 5-point delta so it doesn't defeat the optimization on nearly every refetch.Checklist for submitter
If some of the following don't apply, delete the relevant line.
Changes file added for user-visible changes in
changes/,orbit/changes/oree/fleetd-chrome/changes.See Changes files for more information.
It will be included in the feature branch.
Input data is properly validated,
SELECT *is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.Timeouts are implemented and retries are limited to avoid infinite loops
If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes
Testing
Added/updated automated tests
Where appropriate, automated tests simulate multiple hosts and test for host isolation (updates to one hosts's records do not affect another)
QA'd all new/changed functionality manually
For unreleased bug fixes in a release candidate, one of:
Database migrations
COLLATE utf8mb4_unicode_ci).New Fleet configuration settings
If you didn't check the box above, follow this checklist for GitOps-enabled settings:
fleetctl generate-gitopsfleetd/orbit/Fleet Desktop
runtime.GOOSis used as needed to isolate changesSummary by CodeRabbit