Skip to content

Conversation

@andresokol
Copy link

@andresokol andresokol commented Nov 23, 2025

OwnTracks supports returning other shared locations in the HTTP API. This PR extends OwnTracks integration by responding with locations of other users in the family.

API Response Enhancement:

  • The create action in app/controllers/api/v1/owntracks/points_controller.rb now returns a JSON array of formatted family member locations instead of an empty object.

Family Locations Formatting:

  • Added OwnTracks::FamilyLocationsFormatter service (app/services/own_tracks/family_locations_formatter.rb) to gather and format the latest location points from family members who have sharing enabled, outputting them in Owntracks-compatible format.

Owntracks Format Conversion Utilities:

  • Added methods to OwnTracks::Params for converting battery status, connection type, and velocity to Owntracks-specific representations (battery_status_to_numeric, connection_to_string, velocity_to_kmh).

Summary by CodeRabbit

  • New Features
    • OwnTracks API responses now include family members' locations when sharing is enabled.
    • Location payloads include richer status fields (battery, connection, velocity) with improved value formatting and units for clearer display.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 23, 2025

Walkthrough

PointsController#create now returns formatted OwnTracks locations for family members (via a new OwnTracks::FamilyLocationsFormatter) instead of an empty JSON object; three new OwnTracks::Params helpers were added for battery, connection, and velocity conversions.

Changes

Cohort / File(s) Summary
Controller response
app/controllers/api/v1/owntracks/points_controller.rb
create action updated to invoke OwnTracks::FamilyLocationsFormatter.new(current_user).call and render its result in the JSON response; existing job enqueue remains unchanged.
Family locations service
app/services/own_tracks/family_locations_formatter.rb
New service OwnTracks::FamilyLocationsFormatter added; given a user it checks family feature, selects family members with sharing enabled (excluding current user), fetches each member's latest point, and builds OwnTracks-compatible location hashes (_type, lat, lon, tst, tid, acc, alt, batt, bs, vel, conn).
Param conversion utilities
app/services/own_tracks/params.rb
Added class methods: battery_status_to_numeric(battery_status), connection_to_string(connection), and velocity_to_kmh(velocity) to convert battery state, connection type, and velocity into numeric/short-form values used in payloads.

Sequence Diagram

sequenceDiagram
    participant Client
    participant PointsCtrl as PointsController#create
    participant JobQueue as Job Queue
    participant Formatter as OwnTracks::FamilyLocationsFormatter
    participant DB as Database
    participant Response

    Client->>PointsCtrl: POST /api/v1/owntracks/points
    PointsCtrl->>JobQueue: Enqueue point creation job
    PointsCtrl->>Formatter: new(current_user).call()

    rect rgb(230, 245, 255)
    Formatter->>DB: Check family feature enabled & load family
    Formatter->>DB: Filter members with sharing enabled (exclude current user)
    Formatter->>DB: Fetch latest point per sharing member
    Formatter->>Formatter: Convert points -> OwnTracks payloads (uses Params helpers)
    end

    Formatter-->>PointsCtrl: Array of formatted locations
    PointsCtrl->>Response: Render JSON with family locations
    Response-->>Client: 200 OK with locations
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review focus areas:
    • FamilyLocationsFormatter logic: feature-flag checks, member filtering, null/edge cases when no points.
    • Data mapping: correct field names/units and use of new Params helpers (battery/conn/velocity).
    • Controller response change: ensure backward compatibility and test coverage for empty/no-family cases.

"I hopped through code with a twitchy nose,
Built little maps where family goes,
I formatted bytes with carrot flair 🥕,
Shared locations handled with care,
— Yours, the debugging rabbit"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Return family tracking data to OwnTrack app' directly describes the main change: modifying the OwnTracks API to return family member location data instead of an empty response.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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 and usage tips.

@andresokol andresokol marked this pull request as ready for review November 23, 2025 11:48
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/services/own_tracks/family_locations_formatter.rb (1)

33-49: Minor style improvements suggested.

RuboCop flags a few style issues:

  • Line 45: Trailing comma after the last hash item
  • Lines 34-48: Redundant assignment before returning

Apply this diff to address the style issues:

     def build_owntracks_location(point)
-      location = {
+      {
         _type: 'location',
         lat: point.lat.to_f,
         lon: point.lon.to_f,
         tst: point.timestamp.to_i,
         tid: point.user.email,
         acc: point.accuracy,
         alt: point.altitude,
         batt: point.battery,
         bs: OwnTracks::Params.battery_status_to_numeric(point.battery_status),
         vel: OwnTracks::Params.velocity_to_kmh(point.velocity),
-        conn: OwnTracks::Params.connection_to_string(point.connection),
+        conn: OwnTracks::Params.connection_to_string(point.connection)
       }
-
-      location
     end
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59508ce and 2b96532.

📒 Files selected for processing (3)
  • app/controllers/api/v1/owntracks/points_controller.rb (1 hunks)
  • app/services/own_tracks/family_locations_formatter.rb (1 hunks)
  • app/services/own_tracks/params.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
app/controllers/api/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

Protect API endpoints with Bearer API keys, except explicitly public-share endpoints which must verify UUID access

Files:

  • app/controllers/api/v1/owntracks/points_controller.rb
🧬 Code graph analysis (3)
app/services/own_tracks/params.rb (1)
app/serializers/export_serializer.rb (2)
  • battery_status (47-54)
  • connection (69-76)
app/services/own_tracks/family_locations_formatter.rb (2)
app/services/own_tracks/params.rb (7)
  • initialize (6-8)
  • call (12-35)
  • battery_status_to_numeric (96-104)
  • battery_status (41-50)
  • velocity_to_kmh (115-124)
  • connection_to_string (106-113)
  • connection (67-76)
app/models/concerns/user_family.rb (1)
  • in_family? (16-18)
app/controllers/api/v1/owntracks/points_controller.rb (2)
app/controllers/api_controller.rb (1)
  • current_api_user (29-31)
app/services/own_tracks/family_locations_formatter.rb (1)
  • call (10-19)
🪛 RuboCop (1.81.7)
app/services/own_tracks/params.rb

[convention] 111-111: Redundant else-clause.

(Style/EmptyElse)

app/services/own_tracks/family_locations_formatter.rb

[convention] 33-49: Assignment Branch Condition size for build_owntracks_location is too high. [<1, 17, 0> 17.03/17]

(Metrics/AbcSize)


[convention] 33-49: Method has too many lines. [14/10]

(Metrics/MethodLength)


[convention] 34-46: Redundant assignment before returning detected.

(Style/RedundantAssignment)


[convention] 45-45: Avoid comma after the last item of a hash.

(Style/TrailingCommaInHashLiteral)

🔇 Additional comments (5)
app/services/own_tracks/params.rb (2)

96-104: LGTM! Battery status conversion is correct.

The reverse mapping from string status to numeric codes matches the forward mapping in the battery_status method (lines 41-50).


115-124: LGTM! Velocity conversion is mathematically correct.

The conversion from m/s to km/h using the 3.6 multiplier is correct and properly handles nil cases. This correctly reverses the conversion in the speed method (lines 78-84).

app/controllers/api/v1/owntracks/points_controller.rb (1)

7-13: LGTM! API endpoint is properly protected and implementation is correct.

The endpoint is protected with authenticate_active_api_user! (line 4), which aligns with the coding guidelines. The async job enqueueing followed by returning family locations is correct—the response appropriately shows other family members' locations, while the submitted point is processed asynchronously.

app/services/own_tracks/family_locations_formatter.rb (2)

27-31: Remove the suggested refactor—family_sharing_enabled? is not a database column.

The method is a computed property that accesses a nested JSONB structure (settings['family']['location_sharing']), validates conditions, and parses expiration timestamps. This logic cannot be moved to a database query using .where(family_sharing_enabled: true) as suggested. The current implementation with .select(&:family_sharing_enabled?) is the appropriate approach given the complexity of the conditional logic.

Likely an incorrect or invalid review comment.


39-39: User email exposure via tid field is architectural and unavoidable; no alternative identifier exists.

Verification confirms that the tid field is set to point.user.email, but investigation reveals this is not a fixable issue as suggested:

  1. No alternative identifier available: The User model schema contains only email, encrypted_password, and reset_password_token—there is no username, display_name, or numeric ID-based display name field that could serve as an alternative tracker ID.

  2. Email exposure is system-wide, not isolated: Family members' emails are already exposed throughout the feature in family member lists, map markers (via email initials), and invitation management. Using email in tid is consistent with the existing family feature design, which is fundamentally email-based.

  3. Intentional architectural choice: The system appears designed to use email as the family member identifier. Changing this would require significant refactoring of the family membership and identification system.

The concern about user awareness is valid—consider ensuring users understand that family members will see each other's email addresses, either through UI documentation or consent flows. However, the code itself correctly reflects the system's architecture.

sharing_members = family_members_with_sharing_enabled
return [] unless sharing_members.any?

latest_points = sharing_members.map { |member| member.points.order(timestamp: :desc).first }.compact
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

N+1 query issue when fetching latest points.

This line executes one query per family member to fetch their latest point. For families with many members, this will generate excessive database queries.

Apply this diff to fetch all latest points in a single query:

-    latest_points = sharing_members.map { |member| member.points.order(timestamp: :desc).first }.compact
-    latest_points.map { |point| build_owntracks_location(point) }.compact
+    member_ids = sharing_members.map(&:id)
+    latest_points = Point.where(user_id: member_ids)
+                         .select('DISTINCT ON (user_id) *')
+                         .order('user_id, timestamp DESC')
+    latest_points.map { |point| build_owntracks_location(point) }.compact

Note: The exact SQL may vary depending on your database. For PostgreSQL, DISTINCT ON works well. For MySQL, consider a subquery approach.

Run this script to verify the current query behavior:


🏁 Script executed:

#!/bin/bash
# Description: Check if there are test cases that might reveal the expected query pattern

# Search for existing tests of this formatter
rg -n "FamilyLocationsFormatter" --type=rb -C 3

# Search for similar patterns of fetching latest points per user
rg -n "points\.order.*timestamp.*desc.*first" --type=rb -C 2

Length of output: 53


I'll correct the shell scripts to use proper syntax and verify the issue:

Now let me search the actual codebase with correct syntax:


🏁 Script executed:

#!/bin/bash
# Search for the formatter file and examine it
find . -name "family_locations_formatter.rb" -type f 2>/dev/null | head -20

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Check database configuration
find . -name "database.yml" -o -name "database.yml.example" 2>/dev/null | head -5

Length of output: 21


🏁 Script executed:

#!/bin/bash
# Search for test files related to FamilyLocationsFormatter
find . -name "*family_locations*" -o -name "*own_tracks*" 2>/dev/null | grep -E "(spec|test)" | head -20

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Look for similar N+1 patterns in codebase
rg "\.map.*\.first.*compact" --max-count=10 2>/dev/null || echo "Pattern search completed"

Length of output: 158


🏁 Script executed:

#!/bin/bash
# Read the formatter file
cat ./app/services/own_tracks/family_locations_formatter.rb

Length of output: 1279


🏁 Script executed:

#!/bin/bash
# Check database configuration
cat ./config/database.yml | head -30

Length of output: 694


🏁 Script executed:

#!/bin/bash
# List test files in the own_tracks spec directory
ls -la ./spec/services/own_tracks/

Length of output: 245


🏁 Script executed:

#!/bin/bash
# Search for Point model to understand the relationship
find . -name "point.rb" -path "*/models/*" 2>/dev/null | head -5

Length of output: 21


🏁 Script executed:

#!/bin/bash
# Examine Point model to understand relationships
cat ./app/models/point.rb

Length of output: 3352


🏁 Script executed:

#!/bin/bash
# Examine User model relationships
rg -n "class User" --type=ruby -A 20 | head -40

Length of output: 2535


🏁 Script executed:

#!/bin/bash
# Check for family_sharing_enabled? implementation
rg -n "family_sharing_enabled" --type=ruby -B 2 -A 5

Length of output: 3192


🏁 Script executed:

#!/bin/bash
# Check Point-User relationship and indexes
rg -n "has_many.*point|belongs_to.*user" --type=ruby -C 2 | head -40

Length of output: 1804


N+1 query issue when fetching latest points.

This line executes one query per family member to fetch their latest point. For families with many members, this will generate excessive database queries.

Apply this diff to fetch all latest points in a single query:

-    latest_points = sharing_members.map { |member| member.points.order(timestamp: :desc).first }.compact
-    latest_points.map { |point| build_owntracks_location(point) }.compact
+    member_ids = sharing_members.map(&:id)
+    latest_points = Point.where(user_id: member_ids)
+                         .select('DISTINCT ON (user_id) *')
+                         .order('user_id, timestamp DESC')
+    latest_points.map { |point| build_owntracks_location(point) }.compact

The codebase uses PostgreSQL with PostGIS, so the DISTINCT ON approach is supported.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/services/own_tracks/family_locations_formatter.rb around line 17, the
current map that calls member.points.order(...).first issues one query per
member; replace it with a single query using PostgreSQL DISTINCT ON to select
the latest point per member_id (filter by the set of sharing member IDs and use
DISTINCT ON(member_id) with ORDER BY member_id, timestamp DESC) and then build a
hash keyed by member_id to map those points back to the members so you return
the latest points without N+1 queries.

Comment on lines +106 to +113
def connection_to_string(connection)
case connection
when 'mobile' then 'm'
when 'wifi' then 'w'
when 'offline' then 'o'
else nil
end
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent return value for unknown connection type.

This method returns nil for unknown connection types (line 111), but the export_serializer.rb (lines 68-75) returns 'unknown'. While this may be intentional for the OwnTracks format, consider whether consistency across the codebase is preferred.

Also, the else nil clause on line 111 is redundant since Ruby returns nil by default.

Apply this diff to remove the redundant else clause:

     def connection_to_string(connection)
       case connection
       when 'mobile' then 'm'
       when 'wifi' then 'w'
       when 'offline' then 'o'
-      else nil
       end
     end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def connection_to_string(connection)
case connection
when 'mobile' then 'm'
when 'wifi' then 'w'
when 'offline' then 'o'
else nil
end
end
def connection_to_string(connection)
case connection
when 'mobile' then 'm'
when 'wifi' then 'w'
when 'offline' then 'o'
end
end
🧰 Tools
🪛 RuboCop (1.81.7)

[convention] 111-111: Redundant else-clause.

(Style/EmptyElse)

🤖 Prompt for AI Agents
app/services/own_tracks/params.rb around lines 106 to 113: the method
connection_to_string contains an explicit `else nil` which is redundant (Ruby
returns nil implicitly) and creates an inconsistency with other serializers;
remove the `else nil` clause so the method ends with the case branches only,
returning nil implicitly for unknown connection types (or, if you prefer
consistency with export_serializer, replace the fallback with the string
'unknown' in both places — but at minimum remove the redundant `else nil`).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
app/services/own_tracks/params.rb (1)

94-125: Reverse-conversion helpers look correct; drop redundant else nil in connection_to_string.

The mappings in battery_status_to_numeric and velocity_to_kmh look consistent with the existing forward conversions and OwnTracks expectations. One small improvement in connection_to_string:

  • The explicit else nil is redundant in Ruby and flagged by RuboCop, and it also differs from ExportSerializer#connection, which uses 'unknown' as a fallback. If nil is intentional for the OwnTracks format, you can keep that semantic but still simplify the code by removing the else branch.

Suggested change:

    def connection_to_string(connection)
      case connection
      when 'mobile' then 'm'
      when 'wifi' then 'w'
      when 'offline' then 'o'
-     else nil
      end
    end

This preserves the current behavior (unknown → nil) while satisfying the style cop and prior review feedback.

app/services/own_tracks/family_locations_formatter.rb (1)

14-18: Avoid N+1 queries when loading latest points for sharing members.

sharing_members.map { |member| member.points.order(timestamp: :desc).first } issues one query per member. For larger families this will not scale well. Since you’re on PostgreSQL with PostGIS, you can fetch all latest points in a single query using DISTINCT ON:

-    latest_points = sharing_members.map { |member| member.points.order(timestamp: :desc).first }.compact
-    latest_points.map { |point| build_owntracks_location(point) }.compact
+    member_ids = sharing_members.map(&:id)
+    latest_points = Point.where(user_id: member_ids)
+                         .select('DISTINCT ON (user_id) *')
+                         .order('user_id, timestamp DESC')
+    latest_points.map { |point| build_owntracks_location(point) }.compact

This preserves behavior while eliminating the N+1.

🧹 Nitpick comments (1)
app/services/own_tracks/family_locations_formatter.rb (1)

33-49: Simplify build_owntracks_location and double‑check identifier/PII choice for tid.

Functionally the payload looks fine and uses the new helpers correctly. Two follow‑ups:

  1. Style/cleanup (matches RuboCop hints): you can drop the temporary location variable and trailing comma:
-  def build_owntracks_location(point)
-    location = {
-      _type: 'location',
-      lat: point.lat.to_f,
-      lon: point.lon.to_f,
-      tst: point.timestamp.to_i,
-      tid: point.user.email,
-      acc: point.accuracy,
-      alt: point.altitude,
-      batt: point.battery,
-      bs: OwnTracks::Params.battery_status_to_numeric(point.battery_status),
-      vel: OwnTracks::Params.velocity_to_kmh(point.velocity),
-      conn: OwnTracks::Params.connection_to_string(point.connection),
-    }
-
-    location
-  end
+  def build_owntracks_location(point)
+    {
+      _type: 'location',
+      lat: point.lat.to_f,
+      lon: point.lon.to_f,
+      tst: point.timestamp.to_i,
+      tid: point.user.email,
+      acc: point.accuracy,
+      alt: point.altitude,
+      batt: point.battery,
+      bs: OwnTracks::Params.battery_status_to_numeric(point.battery_status),
+      vel: OwnTracks::Params.velocity_to_kmh(point.velocity),
+      conn: OwnTracks::Params.connection_to_string(point.connection)
+    }
+  end
  1. Identifier privacy: using point.user.email as tid exposes family members’ email addresses to the requesting user. If other OwnTracks payloads already do this it may be acceptable, but if a non‑PII identifier (e.g., initials, username, or a dedicated OwnTracks ID) exists, it might be safer to use that instead. Please confirm that email here is intentional.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b96532 and c181a2f.

📒 Files selected for processing (3)
  • app/controllers/api/v1/owntracks/points_controller.rb (1 hunks)
  • app/services/own_tracks/family_locations_formatter.rb (1 hunks)
  • app/services/own_tracks/params.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/controllers/api/v1/owntracks/points_controller.rb
🧰 Additional context used
🧬 Code graph analysis (2)
app/services/own_tracks/params.rb (1)
app/serializers/export_serializer.rb (2)
  • battery_status (47-54)
  • connection (69-76)
app/services/own_tracks/family_locations_formatter.rb (2)
app/services/own_tracks/params.rb (5)
  • battery_status_to_numeric (96-104)
  • battery_status (41-50)
  • velocity_to_kmh (115-124)
  • connection_to_string (106-113)
  • connection (67-76)
app/models/concerns/user_family.rb (1)
  • in_family? (16-18)
🪛 RuboCop (1.81.7)
app/services/own_tracks/params.rb

[convention] 111-111: Redundant else-clause.

(Style/EmptyElse)

app/services/own_tracks/family_locations_formatter.rb

[convention] 33-49: Assignment Branch Condition size for build_owntracks_location is too high. [<1, 17, 0> 17.03/17]

(Metrics/AbcSize)


[convention] 33-49: Method has too many lines. [14/10]

(Metrics/MethodLength)


[convention] 34-46: Redundant assignment before returning detected.

(Style/RedundantAssignment)


[convention] 45-45: Avoid comma after the last item of a hash.

(Style/TrailingCommaInHashLiteral)

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