Skip to content

iOS/iPadOS device vitals: skip no-op writes to reduce writer load - #50581

Draft
nulmete wants to merge 9 commits into
mainfrom
49984-vitals-dedup-writes
Draft

iOS/iPadOS device vitals: skip no-op writes to reduce writer load#50581
nulmete wants to merge 9 commits into
mainfrom
49984-vitals-dedup-writes

Conversation

@nulmete

@nulmete nulmete commented Aug 5, 2026

Copy link
Copy Markdown
Member

Related issue: Relates to #49984

Every DeviceInformation refetch 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 what SetOrUpdateHostDisksSpace/UpdateHostOperatingSystem already 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/ or ee/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

For unreleased bug fixes in a release candidate, one of:

  • Confirmed that the fix is not expected to adversely impact load test results
  • Alerted the release DRI if additional load testing is needed

Database migrations

  • Checked schema for all modified table for columns that will auto-update timestamps during migration.
  • Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects.
  • Ensured the correct collation is explicitly set for character columns (COLLATE utf8mb4_unicode_ci).

New Fleet configuration settings

  • Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for GitOps-enabled settings:

  • Verified that the setting is exported via fleetctl generate-gitops
  • Verified the setting is documented in a separate PR to the GitOps documentation
  • Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional)
  • Verified that any relevant UI is disabled when GitOps mode is enabled

fleetd/orbit/Fleet Desktop

  • Verified compatibility with the latest released version of Fleet (see Must rule)
  • If the change applies to only one platform, confirmed that runtime.GOOS is used as needed to isolate changes
  • Verified that fleetd runs on macOS, Linux and Windows
  • Verified auto-update works from the released version of component to the new version (see tools/tuf/test)

Summary by CodeRabbit

  • Bug Fixes
    • Improved syncing of Apple device vitals so unchanged data is no longer rewritten unnecessarily.
    • Made battery and timestamp comparisons more tolerant, reducing false change detection.
    • Ensured subscription updates handle replacements more reliably by removing outdated entries and preserving unchanged ones.

nulmete and others added 8 commits August 4, 2026 10:47
…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.
@nulmete

nulmete commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Apple 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

  • fleetdm/fleet#50046: Extends the same vitals persistence logic with no-op detection and subscription update skipping.
  • fleetdm/fleet#50047: Introduces the vitals parsing and persistence path enhanced by this change.
  • fleetdm/fleet#50082: Modifies Apple MDM device vitals persistence and related host API loading.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change and related issue, but the required checklist remains largely unchecked, including automated testing and QA items. Complete the applicable checklist items, especially automated tests, host-isolation testing, manual QA, load-impact assessment, and database checks, or remove non-applicable sections.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: skipping no-op writes for iOS and iPadOS device vitals.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 49984-vitals-dedup-writes

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between fe0f92c and 2b5ed55.

📒 Files selected for processing (2)
  • server/datastore/mysql/apple_mdm_device_vitals.go
  • server/datastore/mysql/apple_mdm_device_vitals_test.go

Comment on lines +164 to +176
// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
// 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.94444% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.27%. Comparing base (fe0f92c) to head (5d137dd).

Files with missing lines Patch % Lines
server/datastore/mysql/apple_mdm_device_vitals.go 81.94% 8 Missing and 5 partials ⚠️
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     
Flag Coverage Δ
backend 69.54% <81.94%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Base automatically changed from 49984-ios-ipados-vitals to main August 7, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant