Skip to content

fix(healthkit): stop reporting observer expirations to Sentry - #2352

Merged
Asherlc merged 8 commits into
mainfrom
Asherlc/seville
Jul 31, 2026
Merged

Asherlc merged 8 commits into
mainfrom
Asherlc/seville

Conversation

@Asherlc

@Asherlc Asherlc commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • Mark HealthKit observer sync in progress at native delivery and in the JavaScript listener before queueing work, so the in-progress flag is set even when the JS thread wakes late from background suspension.
  • Record observer callback expirations as Sentry breadcrumbs only; never capture com.dofek.healthkit-observer: Code: 1 as an error.
  • Clear the in-progress flag only when no observer updates remain pending natively or in the JavaScript queue.

Resolves DOFEK-MOBILE-1C.

Test plan

  • pnpm vitest run --project mobile packages/mobile/lib/background-health-kit-sync.test.ts
  • swift test in packages/mobile/modules/health-kit
  • Resolve DOFEK-MOBILE-1C in Sentry after native iOS release ships

Made with Cursor


Summary by cubic

Stops reporting HealthKit observer expirations to Sentry as errors and hardens sync-in-progress tracking across iOS background suspension and JS catch-up. Also pins tool installs via MISE_LOCKED, aligns report data types and empty states with main, and mirrors anchore/grype pulls in CI to avoid timeouts.

  • Bug Fixes

    • Set observer sync-in-progress at native delivery and in the JS listener; keep it true during catch-up; clear only when no pending updates remain; read/clear under a native lock and gate resets on hasPendingUpdates.
    • Record expirations as Sentry breadcrumbs only (never capture com.dofek.healthkit-observer: Code: 1), and include observerSyncInProgress and hasPendingUpdates in breadcrumb data.
    • Shared reports accept nullable current snapshots; align web to WeeklyReportData/MonthlyReportData, drop recovery, and render server-provided emptyState.
    • Always pass routine sinceDays for provider sync; remove the undefined full-sync path. Withings tests now expect ProviderRequestTimeoutError from @dofek/provider-http.
  • Dependencies

    • Use MISE_LOCKED=1 for pinned installs across Conductor, devcontainer, and docs; removed locked settings from mise.toml and dropped mise trust.
    • Pull anchore/grype via mirror.gcr.io in CI to avoid Docker Hub timeouts.

Written for commit 46a4722. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved HealthKit background synchronization when multiple updates are pending, helping prevent missed or delayed processing.
    • Observer expiration events are now handled without generating misleading error reports.
    • Improved timeout validation for connected health-data providers.
  • Report Improvements

    • Weekly and monthly health reports now consistently display appropriate empty-state content when data is unavailable.
    • Report rendering now handles optional recovery information more reliably.
  • Documentation

    • Added a production incident report covering HealthKit observer expiration handling and validation.

Mark observer sync in progress at native delivery and in the JavaScript listener so iOS background timing boundaries no longer produce false DOFEK-MOBILE-1C alerts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings July 31, 2026 01:28
@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sourcery-ai

sourcery-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR adjusts HealthKit observer sync state handling and Sentry reporting so that observer expirations are tracked as non-error breadcrumbs, and the sync-in-progress flag is aligned with both native and JavaScript pending updates to avoid false-positive Sentry issues under background suspension.

Sequence diagram for updated HealthKit observer sync and expiration handling

sequenceDiagram
    participant HealthKit as HealthKit
    participant HealthKitModule as HealthKitModule
    participant Coordinator as HealthKitObserverUpdateCoordinator
    participant JS as background_health_kit_sync
    participant Sentry as SentrySDK

    HealthKit->>HealthKitModule: observer callback(typeIdentifier, completionHandler)
    HealthKitModule->>Coordinator: register(typeIdentifier, completion)
    HealthKitModule->>HealthKitModule: observerSyncInProgress = true
    HealthKitModule->>JS: MainThreadEventEmitter.emit(event)

    JS->>JS: pendingUpdates.set(updateId, typeIdentifier)
    JS->>HealthKitModule: setObserverSyncInProgress(true)
    JS->>JS: drainSyncQueue()

    JS->>HealthKitModule: setObserverSyncInProgress(pendingUpdates.size > 0)

    Coordinator-->>HealthKitModule: reportExpiration(expiration)
    HealthKitModule->>Sentry: addBreadcrumb(breadcrumb)
    alt !observerUpdateCoordinator.hasPendingUpdates
        HealthKitModule->>HealthKitModule: observerSyncInProgress = false
    end
Loading

File-Level Changes

Change Details Files
Handle HealthKit observer expirations as informational breadcrumbs and decouple them from Sentry error reporting while keeping observer sync state accurate.
  • Replace conditional early-return expiration handling with unconditional Sentry breadcrumb logging that varies the message based on whether a sync is in progress
  • Stop capturing com.dofek.healthkit-observer: Code: 1 errors to Sentry on expiration and instead update observerSyncInProgress to false only when the coordinator reports no pending updates
  • Change the setObserverSyncInProgress bridge function so it only clears the in-progress flag when there are no pending updates, preventing premature resets during long or delayed syncs
packages/mobile/modules/health-kit/ios/HealthKitModule.swift
Expose pending-update state from the HealthKit observer coordinator and cover it with unit tests.
  • Add a hasPendingUpdates computed property that returns true when there are registered pending observer updates
  • Add a unit test verifying that hasPendingUpdates reflects registration and completion of observer updates
packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift
packages/mobile/modules/health-kit/Tests/HealthKitObserverUpdateCoordinatorTests.swift
Align JavaScript-side sync-in-progress tracking with the native module using pending-update information.
  • Set observerSyncInProgress to true immediately when an observer event listener receives an update before queueing work
  • Update drainSyncQueue to set the native in-progress flag based on whether any pending updates remain after a sync cycle
  • Tighten the corresponding Jest/Vitest test to assert that setObserverSyncInProgress(true) is called when a new update arrives and clear previous expectations before triggering the listener
packages/mobile/lib/background-health-kit-sync.ts
packages/mobile/lib/background-health-kit-sync.test.ts
Update documentation to reflect the new semantics of observer expirations and record the incident that motivated this change.
  • Change the HealthKit README to describe observer expirations as expected background failures that generate Sentry breadcrumbs instead of errors and adjust test instructions to assert that no Sentry error is reported
  • Add a production incident baseline entry documenting DOFEK-MOBILE-1C, including symptoms, root cause, fix, validation, and follow-up actions
packages/mobile/modules/health-kit/README.md
docs/production-incident-baseline.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes HealthKit observer synchronization and expiration telemetry, centralizes weekly and monthly report empty-state fixtures, updates report data types and tests, and changes the Withings timeout test to use a typed error assertion.

Changes

HealthKit observer synchronization

Layer / File(s) Summary
Native observer expiration handling
packages/mobile/modules/health-kit/ios/..., packages/mobile/modules/health-kit/Tests/..., packages/mobile/modules/health-kit/README.md, docs/production-incident-baseline.md
The coordinator exposes pending-update state. Native expiration handling records Sentry breadcrumbs and clears synchronization only when no updates remain. Tests, validation guidance, and incident documentation reflect the behavior.
JavaScript observer queue state
packages/mobile/lib/background-health-kit-sync.*
Observer events mark synchronization as active before queue processing. Finalization preserves the active state when pending updates remain.

Report empty-state data flow

Layer / File(s) Summary
Report empty-state contract and route wiring
packages/web/src/components/report-empty-state-fixtures.ts, packages/web/src/routes/health-report.tsx, packages/server/src/routers/health-report.test.ts
Shared weekly and monthly fixtures are added and passed through report generation and route construction. Recovery data is omitted from card data.
Report component types and fixtures
packages/web/src/components/WeeklyReportCard*, packages/web/src/components/MonthlyReportContent*
Report cards use WeeklyReportData and MonthlyReportData. Stories and tests reuse the shared empty-state fixtures and remove obsolete recovery payloads.

Provider timeout assertion

Layer / File(s) Summary
Typed timeout validation
src/providers/withings.test.ts
The timeout test validates ProviderRequestTimeoutError instead of matching an error message.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HealthKitObserver
  participant HealthKitModule
  participant HealthKitObserverUpdateCoordinator
  participant BackgroundHealthKitSync
  participant Sentry
  HealthKitObserver->>HealthKitModule: register observer update
  HealthKitModule->>HealthKitObserverUpdateCoordinator: store pending update
  HealthKitModule->>BackgroundHealthKitSync: set sync in progress
  HealthKitObserver->>HealthKitModule: report expiration
  HealthKitModule->>Sentry: record expiration breadcrumb
  BackgroundHealthKitSync->>HealthKitObserverUpdateCoordinator: process queued updates
  HealthKitObserverUpdateCoordinator-->>BackgroundHealthKitSync: pending updates remain or queue is empty
Loading

Possibly related PRs

  • Asherlc/dofek#1972: Modifies the same HealthKit observer synchronization and expiration handling paths.
  • Asherlc/dofek#2233: Directly relates to pending observer updates and expiration behavior in the same native and JavaScript modules.
  • Asherlc/dofek#2301: Modifies the same weekly and monthly report components, routes, fixtures, and types.

Suggested labels: area/mobile, area/server, area/web, area/providers, type/bug

🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes the main HealthKit change and uses imperative mood, but it does not use the required area prefix format such as [mobile]. Change the title to use the area prefix, for example: [mobile] Stop reporting HealthKit observer expirations to Sentry.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

HealthKit: treat observer expirations as breadcrumbs; tighten sync-in-progress tracking

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Mark HealthKit observer sync as in-progress at native delivery and at JS listener entry.
• Stop capturing observer expirations as Sentry errors; record them as breadcrumbs only.
• Keep the in-progress flag true until no native or JS observer updates remain pending.
Diagram

graph TD
  A["iOS HealthKit observer"] --> B["HealthKitModule.swift"] --> C["JS background sync"]
  C --> D["pendingUpdates + sync queue"] --> C
  C --> E["setObserverSyncInProgress()"] --> B
  F["ObserverUpdateCoordinator"] --> B --> G["Sentry breadcrumb"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Sentry-side filtering (ignore domain/code or handled error)
  • ➕ No behavior change in the HealthKit pipeline
  • ➕ Fast mitigation without coordinating JS/native state
  • ➖ Still produces noise in telemetry until filters are perfect
  • ➖ Loses local context for debugging timing issues unless breadcrumbs are added anyway
  • ➖ Doesn’t address the underlying race where the in-progress flag is set too late
2. Extend native execution window (background task/BGTask)
  • ➕ Reduces expiration frequency by giving JS more time to respond
  • ➖ Higher implementation and OS-behavior complexity
  • ➖ Still not a guarantee under iOS suspension; expirations can still occur
  • ➖ Doesn’t justify treating expirations as errors when they’re expected

Recommendation: Keep the PR’s approach: treat expirations as expected operational telemetry (breadcrumbs) and fix the sequencing so sync-in-progress is set at native delivery and before JS queues work. This directly addresses the false-positive Sentry issue while preserving diagnostic breadcrumbs and making the in-progress lifecycle consistent with pending-work tracking.

Files changed (7) +76 / -31

Bug fix (3) +26 / -26
background-health-kit-sync.tsSet/clear observer sync-in-progress based on pending JS updates +2/-1

Set/clear observer sync-in-progress based on pending JS updates

• Marks observer sync in-progress immediately when an update event is received, before draining the queue. Updates the drain finalizer to keep sync-in-progress true whenever pendingUpdates is non-empty, avoiding premature clearing while work remains queued.

packages/mobile/lib/background-health-kit-sync.ts

HealthKitModule.swiftStop capturing observer expirations; breadcrumb only + safer in-progress clearing +18/-25

Stop capturing observer expirations; breadcrumb only + safer in-progress clearing

• Removes Sentry error capture for observer expirations and always logs a breadcrumb with context (updateId, type, age) and a message that reflects whether JS was already syncing. Marks observerSyncInProgress true at native delivery, and only clears it when the coordinator reports no pending updates (including in the JS-exposed setObserverSyncInProgress(false) path).

packages/mobile/modules/health-kit/ios/HealthKitModule.swift

HealthKitObserverUpdateCoordinator.swiftExpose thread-safe hasPendingUpdates for observer coordinator +6/-0

Expose thread-safe hasPendingUpdates for observer coordinator

• Adds a lock-protected hasPendingUpdates computed property to report whether any observer updates remain outstanding. Used by HealthKitModule to avoid clearing sync-in-progress while native callbacks are still pending.

packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift

Tests (2) +13 / -0
background-health-kit-sync.test.tsAssert observer sync-in-progress is set on listener entry +2/-0

Assert observer sync-in-progress is set on listener entry

• Clears prior mock calls and adds an expectation that the sample-update listener immediately calls setObserverSyncInProgress(true) when an update arrives. This guards against regressions where JS queues work before marking sync state.

packages/mobile/lib/background-health-kit-sync.test.ts

HealthKitObserverUpdateCoordinatorTests.swiftAdd pending-update state test for observer coordinator +11/-0

Add pending-update state test for observer coordinator

• Introduces a new unit test validating hasPendingUpdates toggles true after register() and returns false after completion. Strengthens coverage for pending tracking used to decide when it’s safe to clear sync-in-progress.

packages/mobile/modules/health-kit/Tests/HealthKitObserverUpdateCoordinatorTests.swift

Documentation (2) +37 / -5
production-incident-baseline.mdDocument HealthKit observer expiration false-positive incident +30/-0

Document HealthKit observer expiration false-positive incident

• Adds a new incident entry (2026-07-30) describing the DOFEK-MOBILE-1C Sentry noise, root cause (background timing + late in-progress flag), and the mitigation (breadcrumbs only + improved pending tracking). Captures validation steps and follow-up to resolve the Sentry issue after release.

docs/production-incident-baseline.md

README.mdClarify observer expirations are breadcrumbs, not Sentry errors +7/-5

Clarify observer expirations are breadcrumbs, not Sentry errors

• Updates module docs to state that 25-second expirations are recorded as Sentry breadcrumbs (informational) rather than captured errors. Adjusts the verification step wording to focus on absence of Sentry errors for observer expirations.

packages/mobile/modules/health-kit/README.md

@sourcery-ai sourcery-ai 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.

Hey - I've left some high level feedback:

  • The setObserverSyncInProgress semantics are now more nuanced (false is ignored when native has pending updates); consider renaming or documenting this function to reflect its coordination role rather than a simple boolean setter to avoid future misuse.
  • For the Sentry breadcrumbs on observer expirations, you might include additional fields like observerSyncInProgress and hasPendingUpdates in breadcrumb.data to make it easier to distinguish background-suspension scenarios during incident analysis.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `setObserverSyncInProgress` semantics are now more nuanced (false is ignored when native has pending updates); consider renaming or documenting this function to reflect its coordination role rather than a simple boolean setter to avoid future misuse.
- For the Sentry breadcrumbs on observer expirations, you might include additional fields like `observerSyncInProgress` and `hasPendingUpdates` in `breadcrumb.data` to make it easier to distinguish background-suspension scenarios during incident analysis.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Mobile Preview

Scan to open on device:

QR code for dofek://preview/pr-2352

Channel pr-2352
Deep Link dofek://preview/pr-2352
Commit 66a2156

To test on device:

  1. Build and install the preview client: PREVIEW_CHANNEL=pr-2352 pnpm expo prebuild --clean -p ios
  2. Or tap deep link on an existing preview build: dofek://preview/pr-2352

Each PR gets its own channel. Build a preview client with PREVIEW_CHANNEL=pr-{N} to test.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Storybook previews for 66a21567 are ready:

This comment updates automatically on each PR push.

@qodo-code-review

qodo-code-review Bot commented Jul 31, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 179 rules

Grey Divider


Remediation recommended

1. Uncited iOS/HealthKit behavior claims ✓ Resolved 📘 Rule violation § Compliance
Description
The newly added incident write-up makes third-party behavior claims about iOS background suspension
and HealthKit observer redelivery/expiration without an adjacent primary-source citation. This
violates the documentation requirement and reduces auditability of the incident baseline.
Code

docs/production-incident-baseline.md[R21261-21271]

+- **User impact:** No crash or data loss. HealthKit can redeliver updates after
+  expiration, and successful syncs still uploaded data. The events were false
+  positives under iOS background suspension and long-running observer syncs.
+- **Evidence:** Prior fixes scoped observer syncs to delivered types, removed
+  JavaScript debouncing, and suppressed Sentry capture only while JavaScript
+  reported an active sync. Expirations still reached Sentry when the JavaScript
+  thread was suspended before `setObserverSyncInProgress(true)` ran, and when
+  sync exceeded the unchanged 25-second native boundary without the flag set.
+- **Root cause:** Observer expiration telemetry treated an expected iOS
+  background timing boundary as an actionable error, and the sync-in-progress
+  flag was set too late in the JavaScript delivery path.
Relevance

●●● Strong

Team repeatedly requires adjacent primary-source citations for third-party behavior claims in
incident/docs baselines.

PR-#2052
PR-#2289
PR-#2338

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added incident section includes claims about iOS/HealthKit behavior (e.g., background suspension
and redelivery after expiration) but provides no adjacent official-source link; the only link
present is to a Sentry issue, which is not a primary source for Apple platform behavior.

Rule 1505719: Cite third-party behavior claims in docs with primary sources
docs/production-incident-baseline.md[21254-21276]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`docs/production-incident-baseline.md` adds new sentences describing iOS/HealthKit third-party behavior (e.g., background suspension timing boundaries and HealthKit redelivery after expiration) but does not include adjacent citations to primary sources (official Apple documentation).

## Issue Context
Compliance requires that third-party behavior claims in `docs/` (and README files) include an adjacent link to an official/primary source.

## Fix Focus Areas
- docs/production-incident-baseline.md[21254-21282]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Observer flag data race ✓ Resolved 🐞 Bug ☼ Reliability
Description
HealthKitModule now writes observerSyncInProgress from the observer-expiration callback
(scheduled on a dispatch queue), racing with writes from observer delivery and the JS-exposed
setObserverSyncInProgress function. This unsynchronized cross-thread mutation can produce
inconsistent state/telemetry and is undefined behavior under Thread Sanitizer.
Code

packages/mobile/modules/health-kit/ios/HealthKitModule.swift[R58-60]

+            if !self.observerUpdateCoordinator.hasPendingUpdates {
+                self.observerSyncInProgress = false
            }
-            SentrySDK.capture(
-                error: NSError(
-                    domain: "com.dofek.healthkit-observer",
-                    code: 1,
-                    userInfo: [
-                        NSLocalizedDescriptionKey:
-                            "HealthKit observer update expired before JavaScript sync completed",
-                        "updateId": expiration.updateId,
-                        "typeIdentifier": expiration.typeIdentifier,
-                        "ageMilliseconds": expiration.ageMilliseconds,
-                    ]
-                )
-            )
Relevance

●●● Strong

Repo has accepted thread-safety fixes in HealthKit iOS module; unsynchronized cross-queue mutation
likely flagged and fixed.

PR-#764

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR adds a new write to observerSyncInProgress inside reportExpiration, which is invoked from
the coordinator’s expiration queue. The same flag is also written when registering observer updates
and from the JS-exposed setter, so the new write creates cross-thread unsynchronized access.

packages/mobile/modules/health-kit/ios/HealthKitModule.swift[42-65]
packages/mobile/modules/health-kit/ios/HealthKitModule.swift[846-859]
packages/mobile/modules/health-kit/ios/HealthKitModule.swift[902-908]
packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift[46-63]
packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift[89-108]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`observerSyncInProgress` is a plain `Bool` that is now mutated inside the observer expiration callback (running on the coordinator’s `expirationQueue`). The same flag is also mutated during observer delivery and from the JS-callable `setObserverSyncInProgress` function. This introduces a native data race.

## Issue Context
The expiration path is invoked via a `DispatchWorkItem` scheduled with `asyncAfter`, so it can execute concurrently with other module code. The coordinator state (`hasPendingUpdates`) is lock-protected, but the `observerSyncInProgress` flag is not.

## Fix Focus Areas
- packages/mobile/modules/health-kit/ios/HealthKitModule.swift[42-65]
- packages/mobile/modules/health-kit/ios/HealthKitModule.swift[846-859]
- packages/mobile/modules/health-kit/ios/HealthKitModule.swift[902-908]

## Implementation notes
Choose one approach and apply consistently:
1) **Lock-protect the flag**: add a dedicated `NSLock` (or reuse a serial queue) and wrap *all* reads/writes of `observerSyncInProgress` (in the expiration closure, observer delivery, and `setObserverSyncInProgress`).
2) **Single-thread the state**: dispatch all reads/writes of `observerSyncInProgress` onto a single queue (e.g., `DispatchQueue.main.async { ... }`), including the expiration callback’s state update.

Ensure the breadcrumb message selection also reads the flag under the same synchronization mechanism (since it currently reads `observerSyncInProgress` without protection).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread docs/production-incident-baseline.md
Comment thread packages/mobile/modules/health-kit/ios/HealthKitModule.swift Outdated
Replace mise.toml locked settings and mise trust with MISE_LOCKED=1 in setup scripts and docs so Conductor, devcontainer, and local workflows install only pinned versions.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@Asherlc
Asherlc enabled auto-merge (squash) July 31, 2026 01:52
Fix undefined fullSync in provider routine sync, add report emptyState fixtures, use WeeklyReportData for display components, update Withings timeout expectations, and apply Biome formatting.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@Asherlc
Asherlc disabled auto-merge July 31, 2026 02:20
@Asherlc
Asherlc enabled auto-merge (squash) July 31, 2026 02:22
Resolve conflicts in report empty-state fixtures/tests and mobile provider sync tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

Remove recovery fields from component fixtures that use WeeklyReportData/MonthlyReportData, strip recovery when rendering shared reports, and avoid referencing observerUpdateCoordinator during its lazy initialization.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web/src/components/WeeklyReportCard.stories.tsx (1)

78-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the imported fixture in the Empty story.

The default story uses weeklyReportEmptyStateFixture, but the Empty story keeps a second copy of the same payload. Replace the inline object with the imported fixture so the story cannot drift from the shared contract.

🤖 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 `@packages/web/src/components/WeeklyReportCard.stories.tsx` around lines 78 -
95, Update the Empty story in WeeklyReportCard stories to use the imported
weeklyReportEmptyStateFixture instead of duplicating the inline emptyState
payload. Preserve the story’s existing behavior and remove only the redundant
object definition.

Source: Coding guidelines

🤖 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 `@packages/mobile/lib/background-health-kit-sync.test.ts`:
- Around line 665-673: Add a regression test around the observer listener setup
that queues an update while pendingCatchUp remains unresolved, then complete the
queued catch-up and assert mockSetObserverSyncInProgress is never called with
false before catch-up finalization. Preserve the existing assertion for the
immediate true transition and use the existing listener, pendingCatchUp, and
catch-up completion symbols.

In `@packages/mobile/lib/background-health-kit-sync.ts`:
- Line 207: Update the sync-state assignment in drainSyncQueue to remain true
whenever either pendingUpdates has entries or pendingCatchUp is set, preventing
the native HealthKit sync from appearing idle before catch-up begins. Add a
regression test covering an immediate observer delivery during setup and verify
the state stays active until catch-up completes.

In `@packages/mobile/modules/health-kit/ios/HealthKitModule.swift`:
- Around line 54-67: Accesses and transitions of observerSyncInProgress are not
synchronized across concurrent HealthKit paths. In
packages/mobile/modules/health-kit/ios/HealthKitModule.swift lines 54-67,
850-854, and 906-911, introduce or reuse one shared observer-state lock and
guard the flag reads, writes, and hasPendingUpdates check together; update
handleObserverUpdateExpiration and the corresponding registration,
expiration-completion, and bridge-call paths so all observerSyncInProgress state
changes are serialized without changing their existing behavior.

In `@packages/mobile/modules/health-kit/README.md`:
- Around line 39-44: Update the expiration description in the HealthKit README
to cover both cases: JavaScript never responding and synchronization starting
without native completion being received by the 25-second deadline. Replace the
phrase “when JavaScript never responds” with wording centered on native
completion not arriving before the deadline.

In
`@packages/mobile/modules/health-kit/Tests/HealthKitObserverUpdateCoordinatorTests.swift`:
- Around line 57-66: Extend
testHasPendingUpdatesReflectsRegistrationAndCompletion to register two updates,
assert pending state after both registrations, complete only one and assert
hasPendingUpdates remains true, then complete the second and assert it becomes
false. Keep the existing XCTest coordinator-state coverage and use the returned
update IDs to verify each completion transition.

In `@packages/web/src/routes/health-report.tsx`:
- Around line 167-174: Update the route-boundary schemas in the weekly and
monthly report handling so their current fields accept null by making
weeklyReportSchema.current use weekSummarySchema.nullable() and
monthlyReportSchema.current use monthSummarySchema.nullable(). Add route tests
covering empty weekly and monthly reports, ensuring validation succeeds and the
existing empty-state rendering is reached.
- Around line 167-174: Use the canonical server payload in the health report
route: in both the WeeklyReportCard and MonthlyReportCard data constructions,
pass reportData.emptyState instead of the corresponding fixture. Remove the
now-unused fixture import and delete the production duplicate in
packages/web/src/components/report-empty-state-fixtures.ts, or restrict it to a
typed test/story projection of the canonical contract; apply these changes at
packages/web/src/routes/health-report.tsx lines 167-174 and 185-192, and
packages/web/src/components/report-empty-state-fixtures.ts lines 1-38.

---

Outside diff comments:
In `@packages/web/src/components/WeeklyReportCard.stories.tsx`:
- Around line 78-95: Update the Empty story in WeeklyReportCard stories to use
the imported weeklyReportEmptyStateFixture instead of duplicating the inline
emptyState payload. Preserve the story’s existing behavior and remove only the
redundant object definition.
🪄 Autofix (Beta)

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: ASSERTIVE

Plan: Pro Plus

Run ID: a1a3dbaa-6ceb-4541-882a-4e4b317def20

📥 Commits

Reviewing files that changed from the base of the PR and between e51675a and 15b45c7.

📒 Files selected for processing (17)
  • docs/production-incident-baseline.md
  • packages/mobile/lib/background-health-kit-sync.test.ts
  • packages/mobile/lib/background-health-kit-sync.ts
  • packages/mobile/modules/health-kit/README.md
  • packages/mobile/modules/health-kit/Tests/HealthKitObserverUpdateCoordinatorTests.swift
  • packages/mobile/modules/health-kit/ios/HealthKitModule.swift
  • packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift
  • packages/server/src/routers/health-report.test.ts
  • packages/web/src/components/MonthlyReportContent.stories.tsx
  • packages/web/src/components/MonthlyReportContent.test.tsx
  • packages/web/src/components/MonthlyReportContent.tsx
  • packages/web/src/components/WeeklyReportCard.stories.tsx
  • packages/web/src/components/WeeklyReportCard.test.tsx
  • packages/web/src/components/WeeklyReportCard.tsx
  • packages/web/src/components/report-empty-state-fixtures.ts
  • packages/web/src/routes/health-report.tsx
  • src/providers/withings.test.ts

Comment thread packages/mobile/lib/background-health-kit-sync.test.ts
Comment thread packages/mobile/lib/background-health-kit-sync.ts Outdated
Comment thread packages/mobile/modules/health-kit/ios/HealthKitModule.swift Outdated
Comment thread packages/mobile/modules/health-kit/README.md Outdated
Comment thread packages/web/src/routes/health-report.tsx Outdated
Avoid Docker Hub timeouts that were failing the image vulnerability scan and cascading security gates.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

Keep observer sync active while catch-up is pending, serialize native observer state under a lock, accept nullable empty report snapshots, and add regression tests plus Apple doc citations.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

Protects the breadcrumb snapshot and clear path from racing with bridge
and delivery writes, and enriches expiration telemetry with pending state.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@Asherlc
Asherlc merged commit f5a85aa into main Jul 31, 2026
101 checks passed
@Asherlc
Asherlc deleted the Asherlc/seville branch July 31, 2026 16:06
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.

2 participants