Skip to content

fix(sentry): suppress transient provider and HealthKit sync noise - #2347

Merged
Asherlc merged 1 commit into
mainfrom
Asherlc/fix-sentry-issues
Jul 30, 2026
Merged

Asherlc merged 1 commit into
mainfrom
Asherlc/fix-sentry-issues

Conversation

@Asherlc

@Asherlc Asherlc commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Wrap provider connect timeouts (ETIMEDOUT, etc.) as ProviderRequestTimeoutError and exclude them from Sentry in sync jobs.
  • Suppress HealthKit observer expiration noise during active JS sync and skip background upload timeouts from Sentry.
  • Cap mobile query cache persistence at 5MB to prevent runaway AsyncStorage writes.

Test plan

  • pnpm vitest run --project unit packages/provider-http/src/rate-limit.test.ts src/jobs/process-sync-job.test.ts
  • pnpm vitest run --project mobile packages/mobile/lib/background-health-kit-sync.test.ts packages/mobile/lib/mobile-query-persistence.test.tsx
  • Resolved DOFEK-SERVER-4A, DOFEK-MOBILE-1C, DOFEK-MOBILE-1E, DOFEK-MOBILE-19 in Sentry

Made with Cursor

Summary by Sourcery

Suppress transient provider connection and HealthKit sync errors from Sentry while adding safeguards against oversized mobile query cache persistence.

Bug Fixes:

  • Exclude provider connection timeout errors from Sentry reporting in sync jobs while still tracking metrics.
  • Avoid reporting transient background HealthKit upload timeouts to Sentry and mark observer sync lifecycle during JavaScript-driven deliveries.
  • Prevent HealthKit observer expiration events from being reported as errors when a JavaScript sync is still in progress.
  • Drop oversized mobile query caches instead of persisting them to AsyncStorage to avoid runaway storage usage.

Enhancements:

  • Cap mobile query cache persistence size via a bounded AsyncStorage wrapper and bump the cache contract version.

Tests:

  • Add unit tests covering provider connect timeout wrapping and non-reporting in sync jobs.
  • Add mobile tests verifying suppression of background HealthKit timeout reporting and observer sync lifecycle signalling.
  • Add mobile tests ensuring oversized query caches are not written to AsyncStorage.

Summary by cubic

Suppress transient provider and HealthKit sync errors from Sentry and add a 5MB cap to mobile query cache persistence. This reduces false alerts and prevents large AsyncStorage writes.

  • Bug Fixes
    • Server: Wrap connect failures as ProviderRequestTimeoutError in @dofek/provider-http and treat transport errors (503/504, timeouts) as non-reportable in process-sync-job, while keeping metrics and retries (DOFEK-SERVER-4A).
    • Mobile HealthKit (iOS): Add setObserverSyncInProgress and suppress observer expiration errors while JS sync is running; ignore background upload timeouts and retry on next delivery (DOFEK-MOBILE-1C, DOFEK-MOBILE-19).
    • Mobile query cache: Cap persisted cache at 5MB and drop oversize writes; bump cache contract to v5 (DOFEK-MOBILE-1E).

Written for commit 1b3c681. Summary will update on new commits.

Review in cubic

Treat provider connect timeouts as transport errors instead of Sentry incidents, keep HealthKit observer expirations quiet during active JS sync, skip background upload timeouts, and cap mobile query cache persistence at 5MB.

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

cursor Bot commented Jul 30, 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.

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

Sorry @Asherlc, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

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.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Asherlc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b3366f62-f163-4c9d-a22a-2db7cec969ca

📥 Commits

Reviewing files that changed from the base of the PR and between 42dcbf8 and 1b3c681.

📒 Files selected for processing (11)
  • packages/mobile/lib/background-health-kit-sync.test.ts
  • packages/mobile/lib/background-health-kit-sync.ts
  • packages/mobile/lib/health-kit-errors.ts
  • packages/mobile/lib/mobile-query-persistence.test.tsx
  • packages/mobile/lib/mobile-query-persistence.ts
  • packages/mobile/modules/health-kit/index.ts
  • packages/mobile/modules/health-kit/ios/HealthKitModule.swift
  • packages/provider-http/src/rate-limit.test.ts
  • packages/provider-http/src/rate-limit.ts
  • src/jobs/process-sync-job.test.ts
  • src/jobs/process-sync-job.ts

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.

@sourcery-ai

sourcery-ai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Suppresses noisy Sentry reporting from transient provider/HealthKit network conditions and caps mobile query cache persistence size, via new error-classification utilities and bounded storage logic, plus supporting native/mobile changes and tests.

Sequence diagram for HealthKit observer expiration handling and JS sync flag

sequenceDiagram
  participant JS as BackgroundHealthKitSync_JS
  participant HKJS as HealthKitModule_JS
  participant HKNative as HealthKitModule_native
  participant Sentry

  JS->>HKJS: drainSyncQueue()
  HKJS->>HKNative: setObserverSyncInProgress(true)

  HKNative->>HKNative: observerUpdateCoordinator.handleExpiration(expiration)
  alt observerSyncInProgress is true
    HKNative->>Sentry: addBreadcrumb(healthkit.observer)
  else observerSyncInProgress is false
    HKNative->>Sentry: capture(error com.dofek.healthkit-observer)
  end

  JS->>HKJS: performHealthKitSync()
  HKJS->>HKNative: completeObserverUpdates(updateIds, succeeded)
  JS->>HKJS: drainSyncQueue() completes
  HKJS->>HKNative: setObserverSyncInProgress(false)
Loading

Flow diagram for provider error classification and Sentry suppression in sync jobs

flowchart TD
  A[Provider HTTP fetch error] --> B{"isProviderConnectFailure(error)?"}
  B -->|yes| C[Throw ProviderRequestTimeoutError]
  B -->|no| D[Other error handling]

  C --> E[Sync job receives ProviderRequestTimeoutError]
  E --> F{"isProviderTransportError(error)?"}
  F -->|yes| G[shouldReportProviderError returns false]
  G --> H[Skip Sentry capture<br/>record metrics only]
  F -->|no| I{"isRetryableInfraError(error)?"}
  I -->|yes| J[Sentry.captureException with retryable tag]
  I -->|no| K[Normal non-retryable handling]
Loading

File-Level Changes

Change Details Files
Classify provider connect failures as timeout errors and exclude them from Sentry/reporting in sync flows.
  • Introduce helper to detect provider connect failures by traversing error causes and inspecting error names/codes.
  • Wrap detected connect failures in ProviderRequestTimeoutError in the rate-limit-aware fetch factory.
  • Treat ProviderRequestTimeoutError as a transport error in sync job processing, skipping Sentry reporting and retry classification while still recording metrics.
  • Extend sync job retry and reporting logic to ignore provider transport errors both when scanning sync results and when handling thrown infrastructure errors.
  • Add tests that validate wrapping of undici connect timeouts and that returned provider connect timeouts are not reported to Sentry but do increment metrics.
packages/provider-http/src/rate-limit.ts
src/jobs/process-sync-job.ts
packages/provider-http/src/rate-limit.test.ts
src/jobs/process-sync-job.test.ts
Reduce HealthKit-related Sentry noise by distinguishing transient background upload timeouts and marking observer sync lifecycle across native and JS.
  • Add JS-side detection of transient background HealthKit network errors based on error message normalization and skip Sentry reporting for those, logging a retry message instead.
  • Wire a new setObserverSyncInProgress bridge function between JS and the iOS HealthKit module to track whether JavaScript is actively draining observer deliveries.
  • Update background HealthKit sync queue draining to toggle observerSyncInProgress around sync execution, so native can suppress expiration errors during active JS sync.
  • Modify the iOS HealthKit observer expiration handler to add a breadcrumb instead of capturing an error when expiration happens while JS sync is in progress, and store observerSyncInProgress state on the module.
  • Extend HealthKit background sync tests to cover non-reporting of background fetch timeouts and correct invocation of the observer sync lifecycle bridge.
packages/mobile/lib/health-kit-errors.ts
packages/mobile/lib/background-health-kit-sync.ts
packages/mobile/modules/health-kit/index.ts
packages/mobile/modules/health-kit/ios/HealthKitModule.swift
packages/mobile/lib/background-health-kit-sync.test.ts
Cap mobile query cache persistence size and drop oversized caches to avoid excessive AsyncStorage writes.
  • Introduce a mobile query cache max persisted size constant (5MB) and bump the cache contract version to invalidate old persisted data if needed.
  • Wrap AsyncStorage in a bounded storage adapter that removes the cache and skips writes when the serialized value exceeds the max size.
  • Use the bounded storage adapter in createMobileQueryPersister instead of raw AsyncStorage, keeping retry/Sentry reporting logic unchanged.
  • Add a test that verifies oversized caches are dropped and not written to AsyncStorage via the persister API.
packages/mobile/lib/mobile-query-persistence.ts
packages/mobile/lib/mobile-query-persistence.test.tsx

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

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Suppress transient provider/HealthKit Sentry noise and cap mobile query cache writes

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Treat provider connect timeouts as transport errors and exclude them from Sentry reporting.
• Reduce HealthKit background sync noise by suppressing observer expirations and transient upload
 timeouts.
• Cap mobile React Query cache persistence size to prevent runaway AsyncStorage growth.
Diagram

graph TD
  A["provider-http rate-limit"] --> B["ProviderRequestTimeoutError"] --> C["processSyncJob"] --> D{{"Sentry"}}
  C --> E["Sync metrics"]
  F["HealthKit native module (iOS)"] --> G["Background HK sync (JS)"] --> D
  H["Mobile query persistence"] --> I[("AsyncStorage")]

  subgraph Legend
    direction LR
    _svc["Service/Module"] ~~~ _db[("Storage")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize Sentry filtering via beforeSend
  • ➕ Single place to suppress known-noisy signatures across app/server
  • ➕ Less invasive than modifying multiple call sites
  • ➖ Brittle: relies on message matching and Sentry event shape
  • ➖ Harder to keep metrics/reporting semantics correct per subsystem
2. Use structured error codes for HealthKit transient failures
  • ➕ More reliable than string matching ("fetch failed"/"timeout")
  • ➕ Easier to localize or change underlying networking stack
  • ➖ May require native changes or upstream library support
  • ➖ Larger scope than this targeted noise-reduction PR
3. Bound persistence by measuring serialized payload size pre-write
  • ➕ More accurate than value.length if encoding changes
  • ➕ Could allow logging/telemetry about dropped payloads
  • ➖ Requires additional serialization pass or persister hook changes
  • ➖ More complexity for limited additional safety

Recommendation: Current approach is a good pragmatic balance: it converts provider connect failures into a typed timeout error (better than Sentry filtering alone), suppresses only clearly transient HealthKit background timeout paths, and adds a hard storage cap to prevent pathological persistence behavior. If timeout suppression needs to expand over time, consider migrating the HealthKit transient detection from message matching to a structured/native signal.

Files changed (11) +248 / -8

Enhancement (2) +22 / -2
mobile-query-persistence.tsBound AsyncStorage query-cache persistence to 5MB and bump contract version +17/-2

Bound AsyncStorage query-cache persistence to 5MB and bump contract version

• Bumps the cache contract version and adds a bounded AsyncStorage wrapper that removes and skips writes when the payload exceeds 5MB, preventing runaway persistence behavior.

packages/mobile/lib/mobile-query-persistence.ts

index.tsExpose setObserverSyncInProgress to native HealthKit module +5/-0

Expose setObserverSyncInProgress to native HealthKit module

• Adds a JS-facing helper to tell the native HealthKit observers whether JavaScript is still processing an observer delivery.

packages/mobile/modules/health-kit/index.ts

Bug fix (5) +113 / -5
background-health-kit-sync.tsSuppress transient background HealthKit upload timeouts and signal observer lifecycle +11/-1

Suppress transient background HealthKit upload timeouts and signal observer lifecycle

• Adds transient network timeout detection for background uploads and logs/returns without capturing to Sentry. Signals native observer state via setObserverSyncInProgress(true/false) while draining the JS sync queue.

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

health-kit-errors.tsAdd classifier for transient HealthKit background network timeouts +9/-0

Add classifier for transient HealthKit background network timeouts

• Introduces isBackgroundHealthKitTransientNetworkError() using normalized error-message heuristics to detect fetch timeout failures during background deliveries.

packages/mobile/lib/health-kit-errors.ts

HealthKitModule.swiftSuppress observer expiration error reporting during active JS sync +21/-1

Suppress observer expiration error reporting during active JS sync

• Tracks whether JS sync is in progress and downgrades observer-expiration reporting to an info breadcrumb when expirations occur during active JS processing. Adds an exported native function to set the in-progress flag.

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

rate-limit.tsDetect provider connect failures and wrap as request timeouts +40/-0

Detect provider connect failures and wrap as request timeouts

• Adds connect-failure detection by walking error causes and checking known timeout/network codes. Uses this to throw ProviderRequestTimeoutError from the rate-limit-aware fetch wrapper.

packages/provider-http/src/rate-limit.ts

process-sync-job.tsExclude provider transport errors from Sentry and retryable infra logic +32/-3

Exclude provider transport errors from Sentry and retryable infra logic

• Introduces detection of ProviderRequestTimeoutError (including nested causes) and treats it as a transport error alongside service-unavailable cases. Updates reporting and retryable-infra selection to avoid escalating these transient transport failures to Sentry.

src/jobs/process-sync-job.ts

Tests (4) +113 / -1
background-health-kit-sync.test.tsAdd tests for HealthKit timeout suppression and observer lifecycle signaling +40/-0

Add tests for HealthKit timeout suppression and observer lifecycle signaling

• Adds coverage ensuring background upload timeouts do not call Sentry and instead log/retry behavior. Verifies JS marks the native observer sync lifecycle as in-progress while draining deliveries.

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

mobile-query-persistence.test.tsxTest dropping oversized query-cache persistence writes +20/-1

Test dropping oversized query-cache persistence writes

• Adds a test that persists a deliberately oversized dehydrated cache and asserts nothing is written to AsyncStorage when it exceeds the configured cap.

packages/mobile/lib/mobile-query-persistence.test.tsx

rate-limit.test.tsTest wrapping connect timeouts as ProviderRequestTimeoutError +18/-0

Test wrapping connect timeouts as ProviderRequestTimeoutError

• Adds a unit test asserting undici-style connect ETIMEDOUT failures are recognized as provider connect failures and wrapped as ProviderRequestTimeoutError.

packages/provider-http/src/rate-limit.test.ts

process-sync-job.test.tsTest metrics-only handling for provider connect timeouts +35/-0

Test metrics-only handling for provider connect timeouts

• Adds a test ensuring ProviderRequestTimeoutError in provider sync results records failure metrics but does not report to Sentry.

src/jobs/process-sync-job.test.ts

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

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 181 rules

Grey Divider


Action required

1. Sync queue can deadlock 🐞 Bug ☼ Reliability
Description
drainSyncQueue() calls setObserverSyncInProgress(true) after setting syncing=true but before the
try/finally; if the native call throws, syncing is never reset and future drains are permanently
skipped. The finally block also calls setObserverSyncInProgress(false) without guarding against
native exceptions, which can abort draining and crash the sync loop.
Code

packages/mobile/lib/background-health-kit-sync.ts[R180-182]

  syncing = true;
+  setObserverSyncInProgress(true);
  try {
Relevance

●●● Strong

PR #2049 accepted ensuring syncing resets via try/finally; this reintroduces pre-try exception
path.

PR-#2049

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR adds a native call between syncing = true and the try, so an exception prevents the
cleanup path that resets syncing. The same file already treats native calls as throwable by
guarding completeObserverUpdates with a try/catch, reinforcing that this new native call should
also be guarded.

packages/mobile/lib/background-health-kit-sync.ts[147-156]
packages/mobile/lib/background-health-kit-sync.ts[159-211]
PR-#2049

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

### Issue description
`drainSyncQueue()` sets `syncing = true` and then calls `setObserverSyncInProgress(true)` before entering the `try/finally`. If that native call throws synchronously, the `finally` block never runs, leaving `syncing` stuck `true` and preventing any future background sync drains.

### Issue Context
This codepath is part of the serialized background HealthKit observer delivery drain; it is designed to be exception-safe (e.g., `completeObserverUpdates` is wrapped in a `try/catch`).

### Fix Focus Areas
- packages/mobile/lib/background-health-kit-sync.ts[159-211]

### Suggested fix
- Move `setObserverSyncInProgress(true)` inside the `try` (or wrap it in its own `try/finally`) so `syncing` is always cleared.
- Wrap both `setObserverSyncInProgress(true|false)` calls in `try/catch` and record via `captureException` (similar to `acknowledgeObserverUpdates`) so a native exception can’t permanently block draining.
- Ensure `setObserverSyncInProgress(false)` cannot throw out of `finally` and interrupt the recursive `await drainSyncQueue()`.

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



Remediation recommended

2. Native observer flag race 🐞 Bug ☼ Reliability
Description
HealthKitModule.swift reads observerSyncInProgress from the observer expiration callback that runs
on HealthKitObserverUpdateCoordinator’s background expirationQueue, while setObserverSyncInProgress
mutates it from JS without synchronization. This introduces an unsynchronized cross-thread access
(data race) that can lead to nondeterministic visibility and unsafe native behavior.
Code

packages/mobile/modules/health-kit/ios/HealthKitModule.swift[R44-59]

+        reportExpiration: { [weak self] expiration in
+            guard let self else {
+                return
+            }
+            if self.observerSyncInProgress {
+                let breadcrumb = Breadcrumb(level: .info, category: "healthkit.observer")
+                breadcrumb.message =
+                    "Observer update expired while JavaScript sync was still running"
+                breadcrumb.data = [
+                    "updateId": expiration.updateId,
+                    "typeIdentifier": expiration.typeIdentifier,
+                    "ageMilliseconds": expiration.ageMilliseconds,
+                ]
+                SentrySDK.addBreadcrumb(breadcrumb)
+                return
+            }
Relevance

●●● Strong

Team previously accepted Swift thread-safety fixes (e.g., make callbacks/data access thread-safe) in
HealthKit module.

PR-#764
PR-#1226

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR introduces observerSyncInProgress and uses it inside reportExpiration. Separately, the
coordinator’s expire() calls reportExpiration from a background expirationQueue, making the
read happen off the main thread while the setter can run on a different queue, with no
synchronization shown.

packages/mobile/modules/health-kit/ios/HealthKitModule.swift[42-77]
packages/mobile/modules/health-kit/ios/HealthKitModule.swift[913-915]
packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift[24-57]
packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift[83-102]

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 accessed from multiple threads/queues without synchronization: written via the exported Expo function, and read inside the observer expiration callback invoked on a dedicated background queue.

### Issue Context
`HealthKitObserverUpdateCoordinator` schedules expirations on `expirationQueue.asyncAfter(...)`, and calls `reportExpiration(...)` from that queue.

### Fix Focus Areas
- packages/mobile/modules/health-kit/ios/HealthKitModule.swift[42-77]
- packages/mobile/modules/health-kit/ios/HealthKitModule.swift[913-915]
- packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift[24-57]
- packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift[83-102]

### Suggested fix
- Protect `observerSyncInProgress` with a lock (e.g., `NSLock`) or a serial `DispatchQueue`.
 - In `setObserverSyncInProgress`, write under the lock/queue.
 - In `reportExpiration`, read under the same lock/queue.
- Alternatively, route the expiration callback’s decision to a single queue (e.g., marshal onto main queue before reading the flag), but ensure this doesn’t block observer completion.

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


3. Cache cap not bytes 🐞 Bug ☼ Reliability
Description
createBoundedAsyncStorage enforces MOBILE_QUERY_CACHE_MAX_PERSISTED_BYTES using value.length, but
JS string length is not encoded byte size; this can allow persisted entries to exceed the intended
5MB limit when non-ASCII content is present. The constant name/documentation implies a byte-accurate
bound, so the current check does not reliably prevent oversized AsyncStorage writes.
Code

packages/mobile/lib/mobile-query-persistence.ts[R20-29]

+function createBoundedAsyncStorage() {
+  return {
+    getItem: (key: string) => AsyncStorage.getItem(key),
+    setItem: async (key: string, value: string) => {
+      if (value.length > MOBILE_QUERY_CACHE_MAX_PERSISTED_BYTES) {
+        await AsyncStorage.removeItem(key);
+        return;
+      }
+      await AsyncStorage.setItem(key, value);
+    },
Relevance

●● Moderate

No clear historical reviews on enforcing byte-accurate limits vs string.length for AsyncStorage
persistence caps.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code defines the limit in bytes but compares it to JS string length. This mismatch means the
implementation can diverge from the intended storage bound.

packages/mobile/lib/mobile-query-persistence.ts[9-32]

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

### Issue description
`MOBILE_QUERY_CACHE_MAX_PERSISTED_BYTES` is treated as a byte limit, but the enforcement uses `value.length` (UTF-16 code units). This does not reliably cap the actual stored size.

### Issue Context
The goal of this PR is to prevent runaway AsyncStorage writes by dropping oversized persisted caches.

### Fix Focus Areas
- packages/mobile/lib/mobile-query-persistence.ts[9-32]

### Suggested fix
- Replace the `value.length` check with a byte-size check for the actual persisted encoding (typically UTF-8), e.g.:
 - `const bytes = new TextEncoder().encode(value).length;`
 - compare `bytes > MOBILE_QUERY_CACHE_MAX_PERSISTED_BYTES`.
- Add/adjust a test case that uses multibyte characters to ensure the cap is enforced in terms of bytes, not code units.

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



Informational

4. isBackgroundHealthKitTransientNetworkError in TS 📘 Rule violation ⌂ Architecture
Description
New HealthKit sync behavior (transient network error classification and retry decisioning) is
implemented in TypeScript rather than in the Swift HealthKit module. This violates the requirement
that HealthKit domain/retry logic reside in Swift with TypeScript remaining bridge-only, and it
increases maintenance risk due to fragile message-based error parsing.
Code

packages/mobile/lib/health-kit-errors.ts[R15-21]

+export function isBackgroundHealthKitTransientNetworkError(error: unknown): boolean {
+  const message = error instanceof Error ? error.message : String(error);
+  const normalized = message.toLowerCase();
+  return (
+    normalized.includes("fetch failed") &&
+    (normalized.includes("timed out") || normalized.includes("timeout"))
+  );
Relevance

● Weak

Similar “move domain logic from TS to Swift (bridge-only)” suggestion was rejected in PR #2026; TS
parsing patterns exist.

PR-#2026
PR-#1405

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 721993 requires BLE/HealthKit domain logic (including retry strategies) to reside
in Swift, with TypeScript limited to bridge wrappers. The PR adds a new TypeScript function that
classifies transient network failures by parsing error messages and uses it to alter HealthKit sync
control flow, which is domain/retry behavior in TS.

Rule 721993: BLE and HealthKit domain logic must reside in Swift (TypeScript is bridge-only)
packages/mobile/lib/health-kit-errors.ts[15-21]
packages/mobile/lib/background-health-kit-sync.ts[86-99]

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

## Issue description
HealthKit sync transient-network error classification/retry logic was added in TypeScript (`isBackgroundHealthKitTransientNetworkError` and its use), but compliance requires HealthKit domain/retry logic to live in Swift with TypeScript acting as bridge-only.

## Issue Context
The current implementation classifies timeouts by parsing `Error.message` and uses that to change sync behavior (skip Sentry + retry on next delivery).

## Fix Focus Areas
- packages/mobile/lib/health-kit-errors.ts[15-21]
- packages/mobile/lib/background-health-kit-sync.ts[86-103]

ⓘ 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 on lines 180 to 182
syncing = true;
setObserverSyncInProgress(true);
try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Sync queue can deadlock 🐞 Bug ☼ Reliability

drainSyncQueue() calls setObserverSyncInProgress(true) after setting syncing=true but before the
try/finally; if the native call throws, syncing is never reset and future drains are permanently
skipped. The finally block also calls setObserverSyncInProgress(false) without guarding against
native exceptions, which can abort draining and crash the sync loop.
Agent Prompt
### Issue description
`drainSyncQueue()` sets `syncing = true` and then calls `setObserverSyncInProgress(true)` before entering the `try/finally`. If that native call throws synchronously, the `finally` block never runs, leaving `syncing` stuck `true` and preventing any future background sync drains.

### Issue Context
This codepath is part of the serialized background HealthKit observer delivery drain; it is designed to be exception-safe (e.g., `completeObserverUpdates` is wrapped in a `try/catch`).

### Fix Focus Areas
- packages/mobile/lib/background-health-kit-sync.ts[159-211]

### Suggested fix
- Move `setObserverSyncInProgress(true)` inside the `try` (or wrap it in its own `try/finally`) so `syncing` is always cleared.
- Wrap both `setObserverSyncInProgress(true|false)` calls in `try/catch` and record via `captureException` (similar to `acknowledgeObserverUpdates`) so a native exception can’t permanently block draining.
- Ensure `setObserverSyncInProgress(false)` cannot throw out of `finally` and interrupt the recursive `await drainSyncQueue()`.

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

Comment on lines +44 to +59
reportExpiration: { [weak self] expiration in
guard let self else {
return
}
if self.observerSyncInProgress {
let breadcrumb = Breadcrumb(level: .info, category: "healthkit.observer")
breadcrumb.message =
"Observer update expired while JavaScript sync was still running"
breadcrumb.data = [
"updateId": expiration.updateId,
"typeIdentifier": expiration.typeIdentifier,
"ageMilliseconds": expiration.ageMilliseconds,
]
SentrySDK.addBreadcrumb(breadcrumb)
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Native observer flag race 🐞 Bug ☼ Reliability

HealthKitModule.swift reads observerSyncInProgress from the observer expiration callback that runs
on HealthKitObserverUpdateCoordinator’s background expirationQueue, while setObserverSyncInProgress
mutates it from JS without synchronization. This introduces an unsynchronized cross-thread access
(data race) that can lead to nondeterministic visibility and unsafe native behavior.
Agent Prompt
### Issue description
`observerSyncInProgress` is accessed from multiple threads/queues without synchronization: written via the exported Expo function, and read inside the observer expiration callback invoked on a dedicated background queue.

### Issue Context
`HealthKitObserverUpdateCoordinator` schedules expirations on `expirationQueue.asyncAfter(...)`, and calls `reportExpiration(...)` from that queue.

### Fix Focus Areas
- packages/mobile/modules/health-kit/ios/HealthKitModule.swift[42-77]
- packages/mobile/modules/health-kit/ios/HealthKitModule.swift[913-915]
- packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift[24-57]
- packages/mobile/modules/health-kit/ios/HealthKitObserverUpdateCoordinator.swift[83-102]

### Suggested fix
- Protect `observerSyncInProgress` with a lock (e.g., `NSLock`) or a serial `DispatchQueue`.
  - In `setObserverSyncInProgress`, write under the lock/queue.
  - In `reportExpiration`, read under the same lock/queue.
- Alternatively, route the expiration callback’s decision to a single queue (e.g., marshal onto main queue before reading the flag), but ensure this doesn’t block observer completion.

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

Comment on lines +20 to +29
function createBoundedAsyncStorage() {
return {
getItem: (key: string) => AsyncStorage.getItem(key),
setItem: async (key: string, value: string) => {
if (value.length > MOBILE_QUERY_CACHE_MAX_PERSISTED_BYTES) {
await AsyncStorage.removeItem(key);
return;
}
await AsyncStorage.setItem(key, value);
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Cache cap not bytes 🐞 Bug ☼ Reliability

createBoundedAsyncStorage enforces MOBILE_QUERY_CACHE_MAX_PERSISTED_BYTES using value.length, but
JS string length is not encoded byte size; this can allow persisted entries to exceed the intended
5MB limit when non-ASCII content is present. The constant name/documentation implies a byte-accurate
bound, so the current check does not reliably prevent oversized AsyncStorage writes.
Agent Prompt
### Issue description
`MOBILE_QUERY_CACHE_MAX_PERSISTED_BYTES` is treated as a byte limit, but the enforcement uses `value.length` (UTF-16 code units). This does not reliably cap the actual stored size.

### Issue Context
The goal of this PR is to prevent runaway AsyncStorage writes by dropping oversized persisted caches.

### Fix Focus Areas
- packages/mobile/lib/mobile-query-persistence.ts[9-32]

### Suggested fix
- Replace the `value.length` check with a byte-size check for the actual persisted encoding (typically UTF-8), e.g.:
  - `const bytes = new TextEncoder().encode(value).length;`
  - compare `bytes > MOBILE_QUERY_CACHE_MAX_PERSISTED_BYTES`.
- Add/adjust a test case that uses multibyte characters to ensure the cap is enforced in terms of bytes, not code units.

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

@github-actions

Copy link
Copy Markdown
Contributor

Mobile Preview

Scan to open on device:

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

Channel pr-2347
Deep Link dofek://preview/pr-2347
Commit 13825ea

To test on device:

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

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

@Asherlc
Asherlc merged commit 8fcd497 into main Jul 30, 2026
51 of 54 checks passed
@Asherlc
Asherlc deleted the Asherlc/fix-sentry-issues branch July 30, 2026 13:45
@github-actions

Copy link
Copy Markdown
Contributor

Storybook previews for 13825eae are ready:

This comment updates automatically on each PR push.

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