Skip to content

feat(companion): record scan location on native QR scans - #2768

Open
carlosvirreira wants to merge 1 commit into
mainfrom
feat/companion-scan-geolocation
Open

feat(companion): record scan location on native QR scans#2768
carlosvirreira wants to merge 1 commit into
mainfrom
feat/companion-scan-geolocation

Conversation

@carlosvirreira

@carlosvirreira carlosvirreira commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Why this exists

Web has recorded scan geolocation since forever (updateScanGeolocation on the public /qr/:id route). The companion recorded who and when but never where — so for every scan made with our own app, "last known location" was blank. Carlos flagged the gap while dogfooding; the mobile resolver's JSDoc had it as an acknowledged deferral ("GPS coordinates are intentionally NOT captured here — a separate, deliberate item"). This is that item.

Intent

Provenance should never be able to hurt the thing it's documenting. Two rules drove every decision:

  1. A scan must never be slower or less reliable because of location. Coordinates come from a cached/last-known fix raced against a 1500 ms timeout; a fresh GPS fix is never awaited in the scan path (it warms in the background for the next scan). Denied permission, stale fix, timeout → we simply record without coordinates.
  2. Precise location must not leak into places we don't control. Coordinates travel as headers, not query params, so they never land in access logs, APM traces or Sentry breadcrumbs. createScan failures no longer carry them in additionalData either.

The transport point is worth flagging: the first implementation used ?latitude=&longitude=. Our own security review caught that it diverged from the working web sibling (which POSTs coordinates in a body) and would have written customer GPS into every request log. Changed before this PR existed.

Behaviour

  • One lazy permission prompt, from the scanner only, and only after camera permission is granted — so the two prompts never stack on first run. Never re-prompted, never gates anything.
  • 5-minute staleness cap: an hour-old fix is wrong provenance, not degraded provenance, so we send nothing.
  • Server validates independently (finite, lat ±90, lng ±180, both-or-neither). Invalid input is ignored, never an error.

Test plan

  • 12 route tests, incl. malformed values, partial pairs, out-of-range, and an explicit empty-string regression (naive z.coerce.number() turns "" into 0 — coordinates off West Africa; we reject instead).
  • Verified end-to-end on a physical iPhone against a local server: a real camera scan wrote 51.97957 / 5.98138 to the Scan row, via headers, with the app reporting no perceptible delay.
  • Webapp validate green; companion tsc + expo lint clean.

Rollout notes for review

  • Needs a native build (new expo-location pod) — not OTA-able. Sequenced ahead of the quantity-trust PR so it can ride the next binary either way.
  • App Store Connect privacy label needs updating before submission: the manifest now declares NSPrivacyCollectedDataTypePreciseLocation (linked, app-functionality, non-tracking). The privacy policy should mention scan location too — flagging as a release task, not a code one.
  • Barcode resolves are untouched: they record no scan provenance at all today, so there's nothing to geo-tag (matches web).
  • Known, unchanged from web: a multi-org member scanning another org's label records against that org — the resolver has always keyed on the QR's owning org, not the caller's active one.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Scan records can now include the device’s location when available.
    • QR scans and deep-link scans support best-effort location capture without interrupting scanning.
    • Added privacy disclosures and permissions for location access on iOS and Android.
  • Bug Fixes

    • Invalid, incomplete, or unavailable location data is safely ignored.
    • Improved scan error logging by limiting recorded diagnostic details.
  • Documentation

    • Updated mobile QR endpoint documentation to describe location support.

Web records where a QR was scanned (updateScanGeolocation on the public
qr route); the companion recorded who and when but never where, so an
asset's last-known-location was blank for every mobile scan. This closes
that parity gap.

- mobile QR resolve accepts optional x-shelf-scan-latitude /
  x-shelf-scan-longitude HEADERS (Zod-validated, range-checked); invalid
  or partial pairs are ignored, never an error — provenance must not be
  able to fail a resolve
- headers rather than query params so precise coordinates never enter
  access logs, APM traces or Sentry breadcrumbs; scan service errors no
  longer carry coordinates in additionalData
- companion sends a cached/last-known fix raced against a 1500ms timeout,
  refreshing in the background; a scan is NEVER delayed or blocked on
  location, and a denied permission simply records without coordinates
- one lazy permission request, only from the scanner and only once camera
  is granted, so the prompts never stack on first run
- 5-minute staleness cap: an hour-old fix is wrong provenance, not
  degraded provenance
- privacy manifest declares precise location (linked, app functionality)

Tested: 12 route tests incl. malformed/partial/out-of-range and an
empty-string 'Null Island' regression; verified end-to-end on an iPhone
against a local server — a real scan wrote 51.9795/5.9813 to Scan.
@github-actions

Copy link
Copy Markdown

🩺 React Doctor — companion

No diagnostics directory passed — scan may have failed (CLI exit unknown). Check the workflow logs.

@github-actions

Copy link
Copy Markdown

🩺 React Doctor — webapp

No diagnostics directory passed — scan may have failed (CLI exit unknown). Check the workflow logs.

additionalData: { id },
label,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Originally flagged line 268; mapped to the nearest line in the diff (136).

🟡 GPS coordinates leak to Sentry/logs via createScanNote cause chain

The PR deliberately strips latitude/longitude from createScan and updateScan error additionalData to prevent GPS leaking into pino logs and Sentry. However, createScanNote — called internally by createScan with the same coordinates — still includes both fields in its catch-block additionalData (line 268). The logger's serializeError function recursively walks error.cause chains, so when createScanNote throws, the nested ShelfError (with GPS in additionalData) is fully serialized into the outer error. Any createScanNote failure (DB error, user lookup failure, etc.) will emit the user's precise GPS to pino and Sentry, exactly the outcome the rest of the PR guards against.

Prompt To Fix With AI
In the `createScanNote` catch block (line ~264-272 of service.server.ts), remove `latitude`, `longitude`, and `manuallyGenerated` from `additionalData` to match the same GPS-scrubbing policy applied to `createScan` and `updateScan`. Only keep identifiers needed to trace the failure: `userId` and `qrId`. Change: `additionalData: { userId, qrId, latitude, longitude, manuallyGenerated }``additionalData: { userId, qrId }`.

Severity: medium | Confidence: 85% | React with 👍 if useful or 👎 if not

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7fd8bef71

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"expo-image-manipulator": "~14.0.8",
"expo-image-picker": "~17.0.10",
"expo-linking": "~8.0.11",
"expo-location": "~19.0.8",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Regenerate the pnpm lockfile for expo-location

Adding this dependency without updating pnpm-lock.yaml makes every frozen workspace install fail with ERR_PNPM_OUTDATED_LOCKFILE before builds or tests can run. This affects the checked CI workflows in .github/workflows/test.yml, .github/workflows/react-doctor.yml, and .github/workflows/docs-deploy.yml, as well as the webapp Docker builds, all of which use pnpm install --frozen-lockfile; regenerate and commit the lockfile with pnpm.

AGENTS.md reference: AGENTS.md:L3-L3

Useful? React with 👍 / 👎.

// `additionalData` is forwarded to the logger and to Sentry. Precise
// user location must never enter the error pipeline; the QR id is what
// makes a failing scan traceable.
additionalData: { qrId },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Redact coordinates from the nested scan-note error

When createScanNote fails after a geolocated mobile scan, this outer redaction is ineffective: createScanNote constructs its own ShelfError with additionalData: { userId, qrId, latitude, longitude, manuallyGenerated } at lines 264–269, that error remains in this error's cause chain, and Logger.error recursively serializes causes before sending the route failure to logs and Sentry. Thus a transient note or database failure leaks the user's precise GPS despite the stated redaction; remove the coordinates from the nested error as well.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b606f122-2f19-40bf-8d94-d5307720672c

📥 Commits

Reviewing files that changed from the base of the PR and between 86727d8 and e7fd8be.

📒 Files selected for processing (11)
  • apps/companion/app.json
  • apps/companion/app/(tabs)/scanner.tsx
  • apps/companion/ios/Shelf/Info.plist
  • apps/companion/ios/Shelf/PrivacyInfo.xcprivacy
  • apps/companion/lib/api/assets.ts
  • apps/companion/lib/deep-links.ts
  • apps/companion/lib/scan-location.ts
  • apps/companion/package.json
  • apps/webapp/app/modules/scan/service.server.ts
  • apps/webapp/app/routes/api+/mobile+/qr.$qrId.test.ts
  • apps/webapp/app/routes/api+/mobile+/qr.$qrId.ts

Walkthrough

The companion app adds cached geolocation acquisition and sends scan coordinates through QR requests. The mobile QR endpoint validates coordinate headers and forwards valid values to scan creation. Platform privacy declarations and scan error metadata are also updated.

Changes

Scan geolocation provenance

Layer / File(s) Summary
Location acquisition and platform declarations
apps/companion/lib/scan-location.ts, apps/companion/package.json, apps/companion/app.json, apps/companion/ios/Shelf/Info.plist, apps/companion/ios/Shelf/PrivacyInfo.xcprivacy
Adds cached, timeout-bounded coordinate retrieval, location permissions, the expo-location dependency, and iOS privacy declarations.
Companion scan request wiring
apps/companion/app/(tabs)/scanner.tsx, apps/companion/lib/deep-links.ts, apps/companion/lib/api/assets.ts
Primes location while scanning, retrieves coordinates for QR flows, and sends them through latitude and longitude request headers.
Server validation and scan persistence
apps/webapp/app/routes/api+/mobile+/qr.$qrId.ts, apps/webapp/app/routes/api+/mobile+/qr.$qrId.test.ts
Validates coordinate headers, ignores invalid or query-string values, forwards valid coordinates to createScan, and adds coverage for these contracts.

Scan error context

Layer / File(s) Summary
Scan error metadata
apps/webapp/app/modules/scan/service.server.ts
Limits createScan and updateScan error metadata to their identifying IDs.

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

Sequence Diagram(s)

sequenceDiagram
  participant Scanner
  participant ScanLocation
  participant CompanionQRClient
  participant MobileQRRoute
  participant CreateScan
  Scanner->>ScanLocation: Retrieve scan coordinates
  ScanLocation-->>Scanner: Coordinates or null
  Scanner->>CompanionQRClient: Resolve QR with coordinates
  CompanionQRClient->>MobileQRRoute: Send latitude/longitude headers
  MobileQRRoute->>CreateScan: Pass validated coordinates
Loading

Possibly related PRs

Suggested reviewers: donkoko

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding native QR scan location recording in the companion app.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/companion-scan-geolocation

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant