Skip to content

fix(healthkit): scope background observer updates - #2233

Merged
Asherlc merged 10 commits into
mainfrom
Asherlc/fix-sentry-7632766197-v1
Jul 29, 2026
Merged

Asherlc merged 10 commits into
mainfrom
Asherlc/fix-sentry-7632766197-v1

Conversation

@Asherlc

@Asherlc Asherlc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Scopes background HealthKit sync to delivered types and registers only sample types consumed by the pipeline.
Uses two-phase native anchors, server-side UUID tombstones, and 500-item deletion batches so failed uploads retry safely.
Adds Swift, mobile, and server regression coverage plus incident and runbook documentation.

Validation

pnpm lint, root/server/web TypeScript checks, 14,207 unit/mobile tests, 75 Swift tests, and the ad-hoc signed Release simulator build pass.

Summary by Sourcery

Scope HealthKit background observer sync to delivered sample types and introduce anchor-aware, type-specific incremental syncing with safe retry semantics for quantity deletions.

New Features:

  • Add observer-focused HealthKit sync path that processes only delivered quantity, sleep, workout, and route types using a one-day bounded window and type-specific handling.
  • Introduce two-phase anchored HealthKit quantity queries that return opaque query IDs and commit native anchors only after successful server upload.
  • Expose a server API for deleting HealthKit quantity samples by UUID, applying provider-scoped tombstones for metric-stream and body-measurement types.

Bug Fixes:

  • Prevent HealthKit background observers from replaying full prior-day windows for all sample types on each change, reducing timeouts and duplicate metric-stream versions.
  • Ensure UUID-addressable HealthKit deletions are retried safely by batching up to 500 deletions per request and only advancing anchors after successful processing.

Enhancements:

  • Restrict native HealthKit background delivery registration to the subset of sample types consumed by the sync pipeline.
  • Refine workout and route background syncing to participate in the new observer-scoped pipeline and improve Sentry error reporting.
  • Refactor quantity sample upload into reusable batched helpers shared by foreground and observer sync paths.

Documentation:

  • Document the HealthKit observer scoping, two-phase anchored query behavior, and incident details in the HealthKit module README and production-incident baseline.

Tests:

  • Add mobile, native Swift, and server tests covering anchored query coordination, deletion batching, type-scoped observer syncing, and the new deletion endpoint behavior.

Summary by cubic

Scopes HealthKit background observer sync to only the delivered types and commits native anchors only after server writes succeed, keeping callbacks fast and preventing all-type replays. Adds provider‑scoped UUID tombstones and strict failure handling so deletions and retries are safe.

  • Bug Fixes

    • Register background delivery only for pipeline types and keep each delivery’s typeIdentifier; coalesce by type and run syncHealthKitObserverChanges(...).
    • Two‑phase anchoring for quantity types now returns a queryId and requires an initialStartDate; commit via completeAnchoredQuery(type, queryId, succeeded) only after server success.
    • Add healthKitSync.deleteQuantitySamples; apply provider‑scoped UUID tombstones, batch up to 500, invalidate user cache, and return actionable errors (PRECONDITION_FAILED, HealthKitDeletionTombstonesUnsupportedError).
    • Treat any stage error as a failed observer sync: don’t call onSyncComplete, acknowledge via completeObserverUpdates([...], false), and capture warnings in Sentry.
    • CI: skip cypress binary downloads during pnpm install; document image scan timeout.
  • Migration

    • Ship a new iOS build (adds queryAnchoredSamples(type, initialStartDate)/completeAnchoredQuery(...) and background type scoping).
    • Validate on a physical iPhone that observer callbacks finish under 25s.

Written for commit 7fd7bbb. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added incremental Apple Health synchronization for observer updates.
    • Improved handling of newly added, deleted, workout, sleep, and route data.
    • Added reliable deletion processing for removed health records.
    • Limited background monitoring to supported Health data types.
  • Bug Fixes
    • Prevented failed syncs from committing incomplete query progress, enabling retries.
    • Improved observer queueing, error reporting, and cleanup behavior.
  • Documentation
    • Expanded guidance for background observers and incremental synchronization.

Asherlc added 2 commits July 27, 2026 19:48
Preserve delivered HealthKit types and advance native anchors only after server writes succeed to prevent all-type replays and callback expiry. Apply UUID deletion tombstones before committing each anchor.
…32766197-v1

# Conflicts:
#	docs/production-incident-baseline.md
Copilot AI review requested due to automatic review settings July 28, 2026 02:53
@cursor

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

@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 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

HealthKit observer synchronization now scopes updates by type, uses native-managed anchored queries with explicit completion, uploads deletion tombstones through a new server mutation, and narrows background observer registration. Tests and incident documentation cover the updated flows.

Changes

HealthKit synchronization

Layer / File(s) Summary
Native anchored-query contract
packages/mobile/modules/health-kit/..., packages/mobile/modules/health-kit/ios/..., packages/mobile/modules/health-kit/Tests/..., packages/mobile/modules/health-kit/README.md
Anchored queries return opaque query IDs, defer anchor persistence until explicit completion, and use an explicit background delivery type set.
Mobile observer sync pipeline
packages/mobile/lib/health-kit-sync.*, packages/mobile/lib/background-health-kit-sync.*, packages/mobile/lib/apple-health-provider.*, packages/mobile/app/..., packages/mobile/test-setup.ts
Observer updates are queued by update ID and type, synchronized through anchored or statistics queries, and completed with deletion support and failure telemetry.
Server deletion processing
packages/server/src/repositories/health-kit-sync-repository.*, packages/server/src/routers/health-kit-sync.*
A bounded deleteQuantitySamples mutation processes UUID tombstones through metric-stream replacement or typed SQL deletion, with cache invalidation, metrics, and error mapping.
Incident records
docs/production-incident-baseline.md
Two 2026-07-27 HealthKit incident entries document observer replay and deletion-test failures, mitigations, validation, and follow-up items.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HealthKitObserver
  participant BackgroundHealthKitSync
  participant AppleHealthSyncService
  participant HealthKitSyncRouter
  HealthKitObserver->>BackgroundHealthKitSync: deliver typed observer update
  BackgroundHealthKitSync->>AppleHealthSyncService: syncObserverChanges(typeIdentifiers)
  AppleHealthSyncService->>HealthKitSyncRouter: push samples and delete UUIDs
  HealthKitSyncRouter-->>AppleHealthSyncService: return sync counts
  AppleHealthSyncService->>HealthKitObserver: complete observer and anchored query
Loading

Possibly related PRs

  • Asherlc/dofek#1405: Earlier Apple Health provider refactoring underlies the observer-sync wiring.
  • Asherlc/dofek#1414: Both changes adjust HealthKit background delivery type selection.
  • Asherlc/dofek#1972: Both changes modify background observer sync execution and telemetry.

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

Suggested reviewers: cubic-dev-ai

🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is relevant and imperative, but it does not use the required area prefix format from AGENTS.md. Rename it to a bracket-prefixed area title, e.g. "[mobile] scope background observer updates", and keep it under 70 characters.
✅ 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.

@sourcery-ai

sourcery-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Scopes background HealthKit background observer sync to delivered sample types, introduces anchored quantity queries with two-phase commit semantics and deletion tombstones, and wires end-to-end support across mobile, native Swift, and server plus regression tests and documentation.

Sequence diagram for anchored HealthKit quantity sync with two-phase commit

sequenceDiagram
  participant JS as syncHealthKitObserverChanges
  participant HK as HealthKitAdapter
  participant API as healthKitSync.deleteQuantitySamples
  participant API2 as healthKitSync.pushQuantitySamples

  JS->>HK: queryAnchoredSamples(typeIdentifier, initialStartDate)
  HK-->>JS: { queryId, samples, deletedUUIDs }

  JS->>API2: mutate({ samples })
  API2-->>JS: { inserted, errors }

  alt [upload succeeded and deletedUUIDs not empty]
    loop batched up to 500
      JS->>API: mutate({ deletedUUIDs, typeIdentifier })
      API-->>JS: { deleted }
    end
  end

  alt [all uploads succeeded and queryId]
    JS->>HK: completeAnchoredQuery(typeIdentifier, queryId, true)
    HK-->>JS: true
  else [upload failed and queryId]
    JS->>HK: completeAnchoredQuery(typeIdentifier, queryId, false)
    HK-->>JS: true
  end
Loading

Sequence diagram for scoped background HealthKit observer sync

sequenceDiagram
  participant Obs as HKObserverQuery
  participant Bg as background-health-kit-sync
  participant Sync as AppleHealthSyncService
  participant JS as syncHealthKitObserverChanges

  Obs-->>Bg: addSampleUpdateListener({ typeIdentifier, updateId })
  Bg->>Bg: pendingUpdates.set(updateId, typeIdentifier)
  Bg->>Bg: drainSyncQueue()

  alt [catch-up sync]
    Bg->>Sync: syncObserverChanges({ typeIdentifiers: BACKGROUND_HEALTH_KIT_TYPES })
  else [type-scoped sync]
    Bg->>Sync: syncObserverChanges({ typeIdentifiers: unique(pendingUpdates.values) })
  end

  Sync->>JS: syncObserverChanges({ typeIdentifiers })
  JS-->>Sync: { inserted, errors }
  Sync-->>Bg: result
  Bg->>Obs: completeObserverUpdates(updateIds, succeeded)
Loading

File-Level Changes

Change Details Files
Scope background HealthKit observers to pipeline-consumed types and run type-scoped observer syncs.
  • Expose BACKGROUND_HEALTH_KIT_TYPES on the JS side to list quantity, sleep, workout, and route types used by background sync.
  • Change background observer queueing to track (updateId,typeIdentifier) pairs and derive the effective sync type set per batch.
  • Invoke a new observer-specific sync path that uses one-day windows and delivered-type filtering instead of replaying all types.
packages/mobile/lib/health-kit-sync.ts
packages/mobile/lib/background-health-kit-sync.ts
packages/mobile/modules/health-kit/ios/HealthKitTypes.swift
packages/mobile/modules/health-kit/README.md
packages/mobile/modules/health-kit/index.ts
packages/mobile/lib/apple-health-provider.ts
Implement anchored quantity sync with two-phase native anchors and server-side UUID tombstones.
  • Extend the HealthKitAdapter and native HealthKitModule to support queryAnchoredSamples(initialStartDate) returning a queryId and completeAnchoredQuery(typeId,queryId,succeeded).
  • Add HealthKitAnchoredQueryCoordinator pending-anchor tracking with explicit completion, including error cases and tests for commit/rollback semantics.
  • Introduce syncHealthKitObserverChanges and syncAnchoredQuantityType to upload additions in 500-item batches, send deleted UUIDs to a new deleteQuantitySamples tRPC endpoint, and only commit anchors on successful upload.
packages/mobile/lib/health-kit-sync.ts
packages/mobile/modules/health-kit/ios/HealthKitAnchorStore.swift
packages/mobile/modules/health-kit/ios/HealthKitModule.swift
packages/mobile/modules/health-kit/index.ts
packages/mobile/modules/health-kit/Tests/HealthKitAnchorStoreTests.swift
packages/mobile/modules/health-kit/index.test.ts
packages/mobile/test-setup.ts
packages/mobile/lib/apple-health-provider.ts
packages/mobile/lib/apple-health-provider.test.ts
Add server support to process HealthKit deletions via metric-stream tombstones or direct health_event deletions.
  • Introduce processDeletedQuantitySamples to fan out UUID deletions either to metric-stream replaceRows with provider-scoped tombstones or to DELETEs against fitness.health_event for other types.
  • Expose a new deleteQuantitySamples endpoint on healthKitSyncRouter that validates inputs, calls processDeletedQuantitySamples, invalidates cache, and records metrics.
  • Add tests verifying tombstone publishing, health_event deletion behavior, and router wiring including cache invalidation.
packages/server/src/routers/health-kit-sync-processors.ts
packages/server/src/routers/health-kit-sync-processors.test.ts
packages/server/src/routers/health-kit-sync.ts
packages/server/src/routers/health-kit-sync.test.ts
Refine background observer behavior, tests, and telemetry around type-scoped sync and failure handling.
  • Update background-health-kit-sync tests to reflect type-scoped workouts, anchored queries, and daily statistics, including concurrency semantics and completion behavior.
  • Adjust mocked HealthKit module and tRPC clients in various test setups to support new anchored-query and deleteQuantitySamples flows.
  • Document the incident, root cause, and fix in the production incident baseline with guidance on validation and remaining risks.
packages/mobile/lib/background-health-kit-sync.test.ts
packages/mobile/modules/health-kit/ios/HealthKitTypesTests.swift
packages/mobile/app/_layout.cleanup.test.tsx
packages/mobile/app/providers/index.test.tsx
packages/mobile/app/providers/[id].test.tsx
packages/mobile/lib/useAutoSync.test.ts
packages/mobile/app/_layout.tsx
docs/production-incident-baseline.md

Possibly linked issues

  • #unknown: PR introduces opaque HKQueryAnchor storage, two-phase completion, JS APIs, and tests, directly satisfying the anchor bug issue.

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

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Mobile Preview

Scan to open on device:

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

Channel pr-2233
Deep Link dofek://preview/pr-2233
Commit 087f158

To test on device:

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

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

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

fix(healthkit): scope background observer sync to delivered types

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Scope background HealthKit observer sync to delivered sample types to avoid full-window replays.
• Add two-phase anchored queries and UUID deletion batching so failed uploads retry safely.
• Introduce server deletion endpoint + tombstones and add cross-stack regression coverage and docs.
Diagram

graph TD
  HK{{"HealthKit (iOS)"}} --> HM["Native HK module"] --> BQ["Observer queue"] --> JS["Observer sync (TS)"] --> API["tRPC healthKitSync"]
  API --> PROC["Deletion processor"] --> DB[("Canonical data")]
  PROC --> MS["Metric tombstones"]

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

The following are alternative approaches to this PR:

1. Persist anchors immediately (single-phase anchored queries)
  • ➕ Simpler API surface (no queryId / completion step).
  • ➕ Fewer moving parts between JS and native.
  • ➖ Breaks retry safety: failed uploads advance anchors and permanently drop changes/deletions.
  • ➖ Reintroduces the incident class (observer timeouts + replay/duplication behavior).
2. Use time-window refresh for all types (no anchors)
  • ➕ Less native complexity; avoids pending-anchor state.
  • ➕ No server deletion endpoint required.
  • ➖ Cannot reliably propagate deletions for UUID-addressable types (HealthKit deletions need HKDeletedObject).
  • ➖ Higher upload volume and greater risk of background callback expiry/duplicates.
3. Server-side cursoring only (client sends lastSyncTime per type)
  • ➕ Moves state to server; simpler client persistence story.
  • ➕ Could unify cross-device state management.
  • ➖ Still cannot infer true HealthKit deletions without anchored deleted-object lists.
  • ➖ More complex server reconciliation; harder to guarantee exact-once semantics.

Recommendation: Keep the PR’s approach: type-scoped observer processing plus two-phase anchored queries is the most reliable way to (1) avoid all-type replays, (2) capture HealthKit deletions, and (3) preserve retry semantics by committing anchors only after durable server writes. The added complexity is justified by the incident history and the correctness requirements around deletions and background callback deadlines.

Files changed (25) +1097 / -124

Enhancement (5) +114 / -2
_layout.tsxWire deleteQuantitySamples into SyncTrpcClient adapter +3/-0

Wire deleteQuantitySamples into SyncTrpcClient adapter

• Extends the mobile app’s SyncTrpcClient wiring to expose the new healthKitSync.deleteQuantitySamples mutation to sync code.

packages/mobile/app/_layout.tsx

apple-health-provider.tsExpose observer-scoped sync and anchored query adapter methods +17/-0

Expose observer-scoped sync and anchored query adapter methods

• Introduces AppleHealthSyncService.syncObserverChanges and adds anchored query methods (queryAnchoredSamples/completeAnchoredQuery) to the default HealthKit adapter.

packages/mobile/lib/apple-health-provider.ts

index.tsAdd completeAnchoredQuery and extend queryAnchoredSamples signature +16/-2

Add completeAnchoredQuery and extend queryAnchoredSamples signature

• Extends queryAnchoredSamples to require an initialStartDate and return a queryId, and adds completeAnchoredQuery to explicitly commit or discard the pending native anchor based on upload success.

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

health-kit-sync-processors.tsImplement processDeletedQuantitySamples with tombstones or canonical deletes +47/-0

Implement processDeletedQuantitySamples with tombstones or canonical deletes

• Adds a new deletion processor that deduplicates UUIDs and either publishes metric-stream tombstones (provider-scoped) for metric-stream/body-measurement types or deletes matching fitness.health_event rows by external_id for other UUID-addressed types.

packages/server/src/routers/health-kit-sync-processors.ts

health-kit-sync.tsAdd deleteQuantitySamples endpoint with 500-UUID limit and metrics +31/-0

Add deleteQuantitySamples endpoint with 500-UUID limit and metrics

• Adds a protected tRPC mutation deleteQuantitySamples (max 500 UUIDs) that ensures provider existence, processes deletions, invalidates user queries when needed, and records endpoint-specific metrics.

packages/server/src/routers/health-kit-sync.ts

Bug fix (5) +436 / -40
background-health-kit-sync.tsCoalesce observer updates by type and call observer-scoped sync +21/-10

Coalesce observer updates by type and call observer-scoped sync

• Replaces updateId-only queuing with a map that preserves type identifiers from observer events. Background sync now calls syncObserverChanges with the coalesced type set (or all background-supported types during catch-up).

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

health-kit-sync.tsImplement syncHealthKitObserverChanges with anchored deletes + batching +278/-13

Implement syncHealthKitObserverChanges with anchored deletes + batching

• Adds type identifiers/constants for observer handling, expands adapter and tRPC client interfaces, and refactors quantity uploads into reusable batching helpers. Implements observer-scoped syncing: additive daily-stats refresh for additive types, anchored queries (two-phase completion) for selected UUID types, quantity-window sync for other non-additive types, and bounded workout/route/sleep handling.

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

HealthKitAnchorStore.swiftIntroduce pending-anchor coordinator with explicit completion +67/-4

Introduce pending-anchor coordinator with explicit completion

• Refactors anchored query coordination to return a pending query result (with a generated queryId) rather than immediately persisting anchors. Adds completion logic with validation (unknown query / mismatched type) and commits anchors only when succeeded=true.

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

HealthKitModule.swiftSupport two-phase anchored queries and scope background observers +41/-7

Support two-phase anchored queries and scope background observers

• Updates queryAnchoredSamples to accept initialStartDate and, when no prior anchor exists, uses a bounded predicate for the initial fetch. Returns queryId for pending anchors and adds completeAnchoredQuery for commit/discard. Changes observer registration to monitor only backgroundDeliveryTypes (pipeline-consumed types).

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

HealthKitTypes.swiftRestrict backgroundDeliveryTypes to pipeline-consumed sample types +29/-6

Restrict backgroundDeliveryTypes to pipeline-consumed sample types

• Replaces the previous derived set from readTypes with an explicit set of 18 quantity types plus sleep, workout, and workout route sample types.

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

Tests (13) +465 / -70
_layout.cleanup.test.tsxExtend app layout mocks for anchored query completion and deletions +7/-0

Extend app layout mocks for anchored query completion and deletions

• Updates HealthKit module mocks to include anchored query APIs and adds a mocked tRPC mutation for deleting quantity samples.

packages/mobile/app/_layout.cleanup.test.tsx

[id].test.tsxUpdate provider tests for anchored sync adapter surface +6/-0

Update provider tests for anchored sync adapter surface

• Adds mocks for queryAnchoredSamples/completeAnchoredQuery so provider flows continue to run under the expanded HealthKit adapter interface.

packages/mobile/app/providers/[id].test.tsx

index.test.tsxUpdate provider index tests for deletion mutation and anchored APIs +7/-0

Update provider index tests for deletion mutation and anchored APIs

• Adds HealthKit anchored query mocks and provides a mocked deleteQuantitySamples mutation in the tRPC client stub used by provider tests.

packages/mobile/app/providers/index.test.tsx

apple-health-provider.test.tsAdapt AppleHealthProvider tests to new sync/delete interfaces +9/-0

Adapt AppleHealthProvider tests to new sync/delete interfaces

• Adds anchored query mocks and a deleteQuantitySamples mutation stub to keep provider-level sync tests aligned with the updated sync pipeline.

packages/mobile/lib/apple-health-provider.test.ts

background-health-kit-sync.test.tsRefactor background sync tests to validate type-scoped observer behavior +54/-47

Refactor background sync tests to validate type-scoped observer behavior

• Updates mocks and assertions so observer deliveries queue by (updateId → typeIdentifier) and drive type-scoped sync, including anchored queries for UUID types and bounded daily-statistics refresh for additive types.

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

health-kit-sync.test.tsAdd tests for observer-scoped sync and two-phase anchored semantics +168/-0

Add tests for observer-scoped sync and two-phase anchored semantics

• Introduces a new suite validating that observer sync uploads only delivered types, batches deletions to 500 UUIDs, and commits/discards anchors based on server success.

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

useAutoSync.test.tsUpdate autosync tests for anchored query adapter additions +6/-0

Update autosync tests for anchored query adapter additions

• Extends HealthKit mocks so autosync tests compile and run with the new anchored query adapter requirements.

packages/mobile/lib/useAutoSync.test.ts

HealthKitAnchorStoreTests.swiftAdd Swift tests for pending anchors and commit-on-success behavior +47/-7

Add Swift tests for pending anchors and commit-on-success behavior

• Refactors existing tests to reflect pending anchored query results and adds coverage ensuring anchors persist only after successful completion (and are not persisted on failed completion).

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

HealthKitTypesTests.swiftUpdate Swift tests to assert background delivery is pipeline-scoped +15/-14

Update Swift tests to assert background delivery is pipeline-scoped

• Replaces the prior assertion that all readable sample types are observed with checks that non-consumed types are excluded, and asserts the expected total count for observed background types.

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

index.test.tsAdd JS bridge tests for completeAnchoredQuery and initialStartDate +21/-2

Add JS bridge tests for completeAnchoredQuery and initialStartDate

• Updates queryAnchoredSamples to accept initialStartDate and validates argument forwarding. Adds a test for the new completeAnchoredQuery bridge used to commit/discard native anchors.

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

test-setup.tsExtend global HealthKit mocks with anchored query completion support +8/-0

Extend global HealthKit mocks with anchored query completion support

• Adds default mocks for queryAnchoredSamples (including queryId) and completeAnchoredQuery to match the expanded native module interface.

packages/mobile/test-setup.ts

health-kit-sync-processors.test.tsAdd server tests for UUID deletion processing and provider scoping +74/-0

Add server tests for UUID deletion processing and provider scoping

• Adds coverage ensuring metric-stream/body-measurement deletions publish provider-scoped tombstones (without cross-provider deletion) and that other UUID-addressed types delete canonical events directly without tombstone publishing.

packages/server/src/routers/health-kit-sync-processors.test.ts

health-kit-sync.test.tsAdd router test for deleteQuantitySamples cache invalidation and tombstones +43/-0

Add router test for deleteQuantitySamples cache invalidation and tombstones

• Introduces a test verifying deleteQuantitySamples publishes provider-scoped tombstones and invalidates the user cache prefix when deletions occur.

packages/server/src/routers/health-kit-sync.test.ts

Documentation (2) +82 / -12
production-incident-baseline.mdDocument HealthKit observer replay incident and mitigation +58/-0

Document HealthKit observer replay incident and mitigation

• Adds an incident entry describing the observer all-type replay failure mode, downstream analytics impact, and the adopted mitigation (type scoping + two-phase anchored queries + deletion batching). Captures validation steps and remaining rollout risk.

docs/production-incident-baseline.md

README.mdDocument observer type scoping and two-phase anchored query completion +24/-12

Document observer type scoping and two-phase anchored query completion

• Updates module docs to clarify that observer deliveries retain sample type identifiers and that only pipeline-consumed types are observed. Documents the two-phase anchored query protocol (upload first, then complete/commit anchor) and retry behavior on failure.

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

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Storybook previews for 087f158c are ready:

This comment updates automatically on each PR push.

@qodo-code-review

qodo-code-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 192 rules

Grey Divider


Action required

1. Observer errors seen as success ✓ Resolved 🐞 Bug ≡ Correctness
Description
performHealthKitSync returns true whenever syncObserverChanges resolves, even when the returned
SyncResult has non-empty errors. This treats partial ingestion failures as success, allowing
post-sync callbacks to run and masking server-side failures that are reported via the errors array.
Code

packages/mobile/lib/background-health-kit-sync.ts[R76-82]

  let result: Awaited<ReturnType<AppleHealthSyncService["sync"]>>;
  try {
-    result = await new AppleHealthSyncService({ trpcClient }).sync({
-      syncRangeDays: 1,
+    result = await new AppleHealthSyncService({ trpcClient }).syncObserverChanges({
+      typeIdentifiers,
      onStage: stageTelemetry.start,
    });
  } catch (error) {
Relevance

⭐⭐⭐ High

Team has accepted HealthKit sync robustness fixes; treating non-empty errors as failure aligns with
prior error-handling patterns.

PR-#1405
PR-#2049
PR-#764

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The background coordinator currently ignores the errors channel from the new observer-scoped sync
result. The observer sync returns errors without throwing in multiple paths, and the server returns
such errors for partial ingestion failures (and even skips cache invalidation when errors exist), so
treating them as success is incorrect.

packages/mobile/lib/background-health-kit-sync.ts[68-119]
packages/mobile/lib/health-kit-sync.ts[521-592]
packages/server/src/routers/health-kit-sync.ts[123-232]

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

## Issue description
`performHealthKitSync(...)` treats a resolved `syncObserverChanges(...)` call as success unconditionally (`return true`), even when `result.errors.length > 0`. The observer-scoped pipeline intentionally surfaces partial failures via `SyncResult.errors` (e.g. server processor failures from `pushQuantitySamples`), so returning success here can:
- run `onSyncComplete` even though ingestion was partial
- suppress “failed sync” signaling/logging for observer completions
- make partial server failures much harder to detect and reason about

## Issue Context
- `syncHealthKitObserverChanges(...)` aggregates errors from batch pushes and workout-route sync into the returned `errors` array.
- The server’s `pushQuantitySamples` explicitly returns `errors` for partial processor failures and skips cache invalidation when errors are present, meaning these errors represent real ingestion problems.

## Fix Focus Areas
- packages/mobile/lib/background-health-kit-sync.ts[68-119]
- packages/mobile/lib/health-kit-sync.ts[521-592]
- packages/server/src/routers/health-kit-sync.ts[123-232]

## Suggested fix
1. Change `performHealthKitSync(...)` to treat `result.errors.length > 0` as a failure signal.
  - Option A (strict): `return result.errors.length === 0;` and only invoke `onSyncComplete` when there are no errors.
  - Option B (still ok): if errors exist, `captureException(new Error(...))` (or structured telemetry) and return `false`.
2. Update logging to explicitly log a failure/partial-failure when `errorCount > 0`.
3. Ensure thrown failures (rejections) continue to return `false` as today; this change is specifically about the “resolved with errors[]” partial-failure path.

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



Remediation recommended

2. processDeletedQuantitySamples throws Error ✓ Resolved 📘 Rule violation ≡ Correctness
Description
processDeletedQuantitySamples() throws a generic Error, which can propagate through the
deleteQuantitySamples tRPC mutation without being mapped to a semantic TRPCError. This can
result in non-actionable client errors and breaks the required error-contract pattern for
procedures.
Code

packages/server/src/routers/health-kit-sync-processors.ts[R356-359]

+    const resolvedPublisher = publisher ?? (await getDefaultMetricStreamEventPublisher());
+    if (!resolvedPublisher.replaceRows) {
+      throw new Error("Metric stream publisher does not support HealthKit deletion tombstones");
+    }
Relevance

⭐⭐⭐ High

Strong precedent to wrap procedure-reachable failures in TRPCError to preserve semantic client error
contract.

PR-#2045
PR-#1960
PR-#2225

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires procedure failures (including helper-thrown errors that reach procedures) to
be represented as TRPCError with semantic codes/messages. The new helper throws a plain Error,
and the new mutation does not show any wrapping logic.

Rule 722038: Use TRPCError with semantic error codes for all tRPC procedure failures
Rule 773528: Use TRPCError with specific codes and actionable messages for server errors
packages/server/src/routers/health-kit-sync-processors.ts[355-360]

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

## Issue description
A newly added helper used by a tRPC mutation throws a generic `Error` (`Metric stream publisher does not support HealthKit deletion tombstones`) instead of a `TRPCError`, and the procedure does not appear to wrap it.

## Issue Context
Compliance requires all client-visible procedure failures to be surfaced as `TRPCError` with semantic `code` and actionable `message`, and helper errors must be wrapped at the procedure boundary.

## Fix Focus Areas
- packages/server/src/routers/health-kit-sync-processors.ts[355-360]
- packages/server/src/routers/health-kit-sync.ts[62-90]

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


3. deleteQuantitySamples missing .output ✓ Resolved 📘 Rule violation ≡ Correctness
Description
The new deleteQuantitySamples tRPC mutation defines an input schema but does not define an output
Zod schema. This violates the requirement that all tRPC procedures declare both input and output
schemas for runtime validation.
Code

packages/server/src/routers/health-kit-sync.ts[R62-90]

+  deleteQuantitySamples: protectedProcedure
+    .input(
+      z.object({
+        deletedUUIDs: z.array(z.string().min(1)).max(500),
+        typeIdentifier: z.string().min(1),
+      }),
+    )
+    .mutation(async ({ ctx, input }) => {
+      await ensureProvider(ctx.db, ctx.userId);
+      const deleted = await processDeletedQuantitySamples(
+        ctx.db,
+        ctx.userId,
+        input.typeIdentifier,
+        input.deletedUUIDs,
+        ctx.metricStreamPublisher,
+      );
+      if (deleted > 0) {
+        await invalidateAllUserQueries(ctx.userId);
+      }
+      healthKitPushTotal.add(1, {
+        endpoint: "deleteQuantitySamples",
+        status: "success",
+      });
+      healthKitRecordsTotal.add(deleted, {
+        endpoint: "deleteQuantitySamples",
+        category: "deletedQuantitySample",
+      });
+      return { deleted };
+    }),
Relevance

⭐⭐⭐ High

Consistent precedent to require .output(...) schemas on tRPC procedures for runtime response
validation.

PR-#2209
PR-#2045
PR-#1123

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist explicitly requires .output(...) Zod schemas on every tRPC procedure. The new
mutation chain includes .input(...) and .mutation(...) but no .output(...) before returning `{
deleted }`.

Rule 722090: Define Zod schemas for all tRPC procedure inputs and outputs
packages/server/src/routers/health-kit-sync.ts[61-90]

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

## Issue description
The new `deleteQuantitySamples` procedure is missing an `.output(...)` Zod schema, so its return shape is not runtime-validated.

## Issue Context
Compliance requires every tRPC procedure to declare both `.input(zodSchema)` and `.output(zodSchema)`.

## Fix Focus Areas
- packages/server/src/routers/health-kit-sync.ts[62-90]

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


4. Raw SQL DELETE in router ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
processDeletedQuantitySamples() executes a raw SQL DELETE directly inside a router-layer module,
which violates the requirement to keep SQL out of routers and to use the typed SQL helper. This
increases maintainability and safety risk by bypassing the repository/typed-query patterns used
elsewhere in the codebase.
Code

packages/server/src/routers/health-kit-sync-processors.ts[R375-384]

+  const externalIds = uniqueUUIDs.map((uuid) => `hk:${uuid}`);
+  await db.execute(
+    sql`DELETE FROM fitness.health_event
+        WHERE user_id = ${userId}
+          AND provider_id = ${PROVIDER_ID}
+          AND external_id IN (${sql.join(
+            externalIds.map((externalId) => sql`${externalId}`),
+            sql`, `,
+          )})`,
+  );
Relevance

⭐⭐⭐ High

Clear precedent: raw SQL execution in router modules is flagged and changes to move/typed-execute
are accepted.

PR-#1158

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist forbids raw SQL/direct DB execution inside router modules and requires schema-aware
execution helpers. The added db.execute(sqlDELETE ...) is raw SQL executed directly in a
routers/ module without executeWithSchema or a repository abstraction.

Rule 722115: Prohibit raw SQL in router files; use repository layer instead
Rule 722037: Use executeWithSchema for raw SQL queries instead of untyped execution
packages/server/src/routers/health-kit-sync-processors.ts[375-384]

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

## Issue description
New code runs a raw SQL `DELETE` via `db.execute(...)` inside a router-layer module (`packages/server/src/routers/...`), which violates router/repository separation and the typed SQL execution requirement.

## Issue Context
The project’s compliance rules require that raw SQL not live in router/controller modules and that raw SQL execution use the project-approved typed helper (`executeWithSchema`) or an equivalent repository abstraction.

## Fix Focus Areas
- packages/server/src/routers/health-kit-sync-processors.ts[375-384]

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


View more (1)
5. replaceRows() awaited in loop ✓ Resolved 📜 Skill insight ➹ Performance
Description
processDeletedQuantitySamples awaits replaceRows inside a loop over UUIDs, causing potentially
hundreds (up to the 500-UUID request cap) of independent tombstone publish operations to run
sequentially and significantly inflate request latency. This violates the requirement to parallelize
independent async work (e.g., via Promise.all) and increases the risk of timeouts under load,
especially for background observer sync flows.
Code

packages/server/src/routers/health-kit-sync-processors.ts[R360-371]

+    const context = await getProviderDataGenerations(db, [{ providerId: PROVIDER_ID, userId }]);
+    for (const uuid of uniqueUUIDs) {
+      await resolvedPublisher.replaceRows(
+        {
+          userId,
+          providerId: PROVIDER_ID,
+          externalId: `hk:${uuid}`,
+        },
+        [],
+        context.operationRevision,
+      );
+    }
Relevance

⭐⭐ Medium

Parallelization requests are inconsistently accepted; similar “await in loop” perf change was
previously rejected in server routers.

PR-#1679
PR-#764

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1917388 requires independent async operations to be parallelized (e.g., with
Promise.all). In the introduced deletion/tombstone path, the code iterates over uniqueUUIDs and
awaits resolvedPublisher.replaceRows for each UUID, forcing each publish to complete before the
next starts; since the router accepts up to 500 UUIDs per request, the worst case becomes hundreds
of serial publish operations. Additionally, replaceRows(...) ultimately publishes via a producer
send (e.g., chunked sends) per call, so repeating it many times serially compounds the end-to-end
latency.

packages/server/src/routers/health-kit-sync-processors.ts[360-371]
packages/server/src/routers/health-kit-sync-processors.ts[342-386]
packages/server/src/routers/health-kit-sync.ts[62-90]
src/metric-stream/redpanda-producer.ts[139-165]
Skill: cloudflare

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

## Issue description
`processDeletedQuantitySamples(...)` publishes deletion tombstones by `await`ing `resolvedPublisher.replaceRows(...)` inside a per-UUID loop, making up to 500 independent publish operations run sequentially in the worst case. This increases latency and raises the likelihood of timeouts (including in background observer sync flows) and conflicts with the compliance requirement to parallelize independent async work (e.g., with `Promise.all()`).

## Issue Context
- The compliance checklist (PR Compliance ID 1917388) requires parallelizing independent async operations.
- The delete endpoint input allows up to 500 UUIDs, and `processDeletedQuantitySamples` de-dupes them into `uniqueUUIDs`.
- `replaceRows(...)` in the Kafka publisher performs producer sends per call (e.g., chunked sending), so calling it hundreds of times serially can be very slow.
- The tombstone writes appear independent per UUID, so they are good candidates for batching or bounded-concurrency parallelism; if concurrency is introduced, errors should still fail the mutation so retries remain safe.

## Fix Focus Areas
- packages/server/src/routers/health-kit-sync-processors.ts[355-372]
- packages/server/src/routers/health-kit-sync.ts[62-90]
- src/metric-stream/redpanda-producer.ts[139-165]

ⓘ 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 packages/server/src/routers/health-kit-sync-processors.ts Outdated
Comment thread packages/server/src/routers/health-kit-sync-processors.ts Outdated
Comment thread packages/server/src/routers/health-kit-sync.ts
Comment thread packages/server/src/routers/health-kit-sync-processors.ts Outdated
Comment thread packages/mobile/lib/background-health-kit-sync.ts Outdated
Assert deletion branch boundaries and side-effect payloads so Stryker detects regressions in tombstone scoping, cache invalidation, and telemetry.
@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 added 2 commits July 27, 2026 20:14
…32766197-v1

# Conflicts:
#	docs/production-incident-baseline.md
Move deletion persistence into the repository with typed results, preserve actionable API failures, and reject partial observer syncs so HealthKit retries safely.
@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.

…32766197-v1

# Conflicts:
#	docs/production-incident-baseline.md
@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.

…32766197-v1

# Conflicts:
#	docs/production-incident-baseline.md
@codereviewbot-ai

codereviewbot-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown

LGTM! The changes cleanly implement two-phase anchored HealthKit synchronization and fix the previous feedback:

  1. Output Schema Validation: Added explicit Zod output validation z.object({ deleted: z.number().int().nonnegative() }) to deleteQuantitySamples in health-kit-sync.ts.
  2. Partial Sync Error Handling: performHealthKitSync now checks result.errors.length > 0, logging warnings, suppressing post-sync completion callbacks, and returning false to acknowledge updates as failed to native HealthKit for safe retries.
  3. Thread-Safe Anchor Persistence: HealthKitAnchoredQueryCoordinator uses thread-safe locks and explicit two-phase commits (completeAnchoredQuery) so native anchors are committed only after server mutation succeeds.
  4. Test Coverage: Comprehensive test suites added across Swift unit tests, TS client sync tests, and server repository tests.

🤖 Reviewed by codereviewbot.ai - Catch bugs before your team does.

@Asherlc
Asherlc enabled auto-merge (squash) July 29, 2026 01:26
@codereviewbot-ai

codereviewbot-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown

PR Review Summary

Overall, this is a clean and robust implementation of HealthKit two-phase anchored query synchronization and background observer change processing.

Key observations:

  • Anchored Sync & Observer Delivery: Native Swift module bridging and TS queue draining correctly track per-type anchors, batch deletions, and report failures to HealthKit observer completions.
  • Server Deletion Processing: Server repository endpoints and tests properly handle deleted quantity sample UUIDs across both metric stream tombstones and database events.
  • Minor Improvement: A small cleanup in HealthKitAnchoredQueryCoordinator.complete was suggested to ensure pending anchors are cleared atomically during initial lock lookup even if anchor persistence throws an error.

🤖 Reviewed by codereviewbot.ai - Catch bugs before your team does.

Comment thread packages/mobile/modules/health-kit/ios/HealthKitAnchorStore.swift

@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: 13

Caution

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

⚠️ Outside diff range comments (1)
packages/mobile/modules/health-kit/Tests/HealthKitAnchorStoreTests.swift (1)

24-123: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for unknownQuery/mismatchedType completion errors.

The new HealthKitAnchoredQueryCoordinatorError.unknownQuery and .mismatchedType cases (in HealthKitAnchorStore.swift) guard the two-phase commit against stale/cross-type queryId misuse, but no test exercises either path here.

🧪 Suggested additional tests
func testCompleteWithUnknownQueryIdThrows() async throws {
    let coordinator = HealthKitAnchoredQueryCoordinator(
        anchorStore: HealthKitAnchorStore(userDefaults: defaults)
    )
    XCTAssertThrowsError(
        try coordinator.complete(typeIdentifier: "heart-rate", queryId: "not-a-real-id", succeeded: true)
    ) { error in
        guard case HealthKitAnchoredQueryCoordinatorError.unknownQuery = error else {
            XCTFail("Unexpected error: \(error)")
            return
        }
    }
}

func testCompleteWithMismatchedTypeIdentifierThrows() async throws {
    let coordinator = HealthKitAnchoredQueryCoordinator(
        anchorStore: HealthKitAnchorStore(userDefaults: defaults)
    )
    let result = try await coordinator.run(typeIdentifier: "heart-rate") { _ in
        return ((), HKQueryAnchor(fromValue: 1))
    }
    XCTAssertThrowsError(
        try coordinator.complete(
            typeIdentifier: "step-count",
            queryId: try XCTUnwrap(result.queryId),
            succeeded: true
        )
    ) { error in
        guard case HealthKitAnchoredQueryCoordinatorError.mismatchedType = error else {
            XCTFail("Unexpected error: \(error)")
            return
        }
    }
}
🤖 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/mobile/modules/health-kit/Tests/HealthKitAnchorStoreTests.swift`
around lines 24 - 123, Add tests alongside the existing coordinator completion
tests for both error paths: verify completing with an unregistered query ID
throws HealthKitAnchoredQueryCoordinatorError.unknownQuery, and verify
completing a valid query ID with a different type identifier throws
.mismatchedType. Use HealthKitAnchoredQueryCoordinator.run and complete,
asserting the specific enum cases.
🤖 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/app/_layout.cleanup.test.tsx`:
- Line 196: Update the deleteQuantitySamples mock used by the cleanup tests to
use mockResolvedValue with a result object containing the required deleted
number, matching the mutation contract and the other mocks in this block.

In `@packages/mobile/lib/background-health-kit-sync.test.ts`:
- Around line 678-683: Update the deferred result type in the firstSync setup to
use the real queryAnchoredSamples shapes: import HealthKitSample directly from
the health-kit module and type samples as HealthKitSample[] and deletedUUIDs as
string[]. Preserve the existing queryId field and mockQueryAnchoredSamples
behavior.

In `@packages/mobile/lib/background-health-kit-sync.ts`:
- Around line 76-79: Update the result annotation in the background sync flow to
use Awaited<ReturnType<AppleHealthSyncService["syncObserverChanges"]>>, matching
the method invoked in the try block. Leave the syncObserverChanges call and
surrounding error handling unchanged.

In `@packages/mobile/lib/health-kit-sync.test.ts`:
- Around line 585-593: Update the health-kit sync test around the deletion
mutation and healthKit.completeAnchoredQuery assertions to verify invocation
order, not only call arguments: assert the deleteQuantitySamples mutation occurs
before completeAnchoredQuery succeeds. Preserve the existing argument assertions
while adding an invocation-order check using the recorded mock call order.

In `@packages/mobile/lib/health-kit-sync.ts`:
- Around line 484-512: The observer route-sync loop should batch routes into a
single mutation instead of calling pushWorkoutRoutes once per workout. Update
the surrounding syncHealthKitToServer flow to collect each non-empty workout
route with its workoutUuid and sourceName, then invoke
trpcClient.healthKitSync.pushWorkoutRoutes.mutate once after collection, while
preserving database-inaccessible rethrows, exception capture, and per-workout
error reporting.
- Around line 442-449: Update the sync completion flow around
completeAnchoredQuery to await its boolean result, fail the operation when the
native call returns false, and only acknowledge the observer after a successful
anchor commit. Correct the returned SyncResult so inserted contains only
uploadResult.inserted; add and propagate an optional deleted field for the
deleted count through background-health-kit-sync.ts, or remove the count if it
is not needed.

In `@packages/mobile/test-setup.ts`:
- Around line 486-497: The anchored-query mock shape is duplicated across three
Health Kit mock factories. In packages/mobile/test-setup.ts lines 486-497,
create and export a colocated createHealthKitAnchorMocks factory or shared
default result object, typing the anchored result from queryAnchoredSamples’
return type; update packages/mobile/app/providers/[id].test.tsx lines 319-327
and packages/mobile/lib/useAutoSync.test.ts lines 52-61 to spread that shared
factory instead of redeclaring queryId, samples, and deletedUUIDs.

In `@packages/server/src/repositories/health-kit-sync-repository.test.ts`:
- Around line 1157-1166: Strengthen the assertion in the “rejects an invalid
typed deletion result” test for
HealthKitSyncRepository.processDeletedQuantitySamples by matching the specific
schema-validation error, following the neighbouring test’s toBeInstanceOf
pattern. Keep the existing invalid mocked result and method arguments, but
ensure the test fails if rejection occurs for an unrelated reason.

In `@packages/server/src/repositories/health-kit-sync-repository.ts`:
- Around line 406-419: Update the deletion flow to capture the rows returned by
executeWithSchema in the repository method containing this SQL, and return the
number of returned records rather than uniqueUUIDs.length. Preserve the existing
delete criteria and return shape while ensuring zero and partial matches report
their actual deleted counts.
- Around line 381-404: Update the deletion handling block for body measurements
and metric streams to invoke replaceRows through the publisher instance, using
publisher.replaceRows within the uniqueUUIDs Promise.all flow; remove the
extracted unbound replaceRows reference while preserving the existing
availability check and arguments.
- Around line 371-405: Update processDeletedQuantitySamples to detect
additiveDailyMetric and pointInTimeDailyMetric types before the health_event
deletion fallback, then reaggregate the affected fitness.daily_metrics rows
using the existing daily-metric logic keyed by date, provider_id, and
source_name. Preserve the current UUID-based canonical-store deletion behavior
for body measurements, metric streams, and other non-daily quantities.

In `@packages/server/src/routers/health-kit-sync.test.ts`:
- Around line 165-168: Update the deleteQuantitySamples input validation to
enforce UUID-formatted deletion identifiers with Zod’s z.uuid(), then replace
the arbitrary deletedUUIDs test fixtures at all referenced cases with valid UUID
values while preserving the existing tombstone-publishing assertions.
- Around line 245-267: Update the production error path used by
deleteQuantitySamples to report unexpected repository failures through Sentry
before returning INTERNAL_SERVER_ERROR. Pass repositoryError to captureException
with the endpoint and user context, then update the test assertion for
mockSentryCaptureException to verify those arguments instead of expecting no
call.

---

Outside diff comments:
In `@packages/mobile/modules/health-kit/Tests/HealthKitAnchorStoreTests.swift`:
- Around line 24-123: Add tests alongside the existing coordinator completion
tests for both error paths: verify completing with an unregistered query ID
throws HealthKitAnchoredQueryCoordinatorError.unknownQuery, and verify
completing a valid query ID with a different type identifier throws
.mismatchedType. Use HealthKitAnchoredQueryCoordinator.run and complete,
asserting the specific enum cases.
🪄 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: e40c6dfc-889a-4f83-835d-5171487f54b2

📥 Commits

Reviewing files that changed from the base of the PR and between eff34bb and 18f81b2.

📒 Files selected for processing (25)
  • docs/production-incident-baseline.md
  • packages/mobile/app/_layout.cleanup.test.tsx
  • packages/mobile/app/_layout.tsx
  • packages/mobile/app/providers/[id].test.tsx
  • packages/mobile/app/providers/index.test.tsx
  • packages/mobile/lib/apple-health-provider.test.ts
  • packages/mobile/lib/apple-health-provider.ts
  • packages/mobile/lib/background-health-kit-sync.test.ts
  • packages/mobile/lib/background-health-kit-sync.ts
  • packages/mobile/lib/health-kit-sync.test.ts
  • packages/mobile/lib/health-kit-sync.ts
  • packages/mobile/lib/useAutoSync.test.ts
  • packages/mobile/modules/health-kit/README.md
  • packages/mobile/modules/health-kit/Tests/HealthKitAnchorStoreTests.swift
  • packages/mobile/modules/health-kit/Tests/HealthKitTypesTests.swift
  • packages/mobile/modules/health-kit/index.test.ts
  • packages/mobile/modules/health-kit/index.ts
  • packages/mobile/modules/health-kit/ios/HealthKitAnchorStore.swift
  • packages/mobile/modules/health-kit/ios/HealthKitModule.swift
  • packages/mobile/modules/health-kit/ios/HealthKitTypes.swift
  • packages/mobile/test-setup.ts
  • packages/server/src/repositories/health-kit-sync-repository.test.ts
  • packages/server/src/repositories/health-kit-sync-repository.ts
  • packages/server/src/routers/health-kit-sync.test.ts
  • packages/server/src/routers/health-kit-sync.ts

Comment thread packages/mobile/app/_layout.cleanup.test.tsx Outdated
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/lib/health-kit-sync.test.ts
Comment thread packages/mobile/lib/health-kit-sync.ts
Comment thread packages/server/src/repositories/health-kit-sync-repository.ts
Comment thread packages/server/src/repositories/health-kit-sync-repository.ts
Comment thread packages/server/src/repositories/health-kit-sync-repository.ts Outdated
Comment thread packages/server/src/routers/health-kit-sync.test.ts
Comment thread packages/server/src/routers/health-kit-sync.test.ts Outdated
Keep anchor commits and deletion telemetry truthful across native, mobile, and server boundaries.
@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 commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the PR-level Swift coverage request in commit 41cbe75. HealthKitAnchorStoreTests now covers unknown query completion and verifies that a type mismatch preserves the pending anchor for a later correctly typed completion. The full Swift package suite passes: 77 tests.

@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 e0e7e8b into main Jul 29, 2026
102 checks passed
@Asherlc
Asherlc deleted the Asherlc/fix-sentry-7632766197-v1 branch July 29, 2026 02:37
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