When designing an activity tracking system, it’s essential to think ahead and plan for flexibility. The system should be able to integrate with different providers, such as Strava, and adapt to new providers in the future. To achieve this flexibility, I introduce a new entity called “Activity.” This entity will serve as a central point for storing activity data, allowing us to switch providers seamlessly.

To create the “Activity” entity, I start by defining its structure in the database. Here’s the migration to create the “activities” table:

class CreateActivities < ActiveRecord::Migration[7.0]
  def change
    create_table :activities do |t|
      t.float "distance"
      t.integer "time"
      t.string "external_id"
      t.datetime "activity_date"
      t.string "name"

      t.timestamps
    end
  end
end

I also define the “Activity” record and its associated repository to interact with the database.

In my example, I focus on integrating with Strava, but keep in mind that the system’s design allows for easy integration with other providers. I use the Strava API to fetch activities, update access tokens, and store new activities. The “sync_activities” endpoint handles this integration.

Activities can vary, and they may be associated with different types of bikes and components. Strava provides data that helps us categorize activities into sport types and mark them as commutes. To add flexibility to my system, I introduce two new fields in the “bikes” table: “sport_type” and “commute.”

To maintain consistency, I update the “Bike” model and add default values for these fields in a new migration:

class AddDefaultTypesToBikes < ActiveRecord::Migration[7.0]
  def change
    add_column :bikes, :sport_type, :string
    add_column :bikes, :commute, :boolean, default: false
  end
end

Now I’m able to save activities. In previous post I get activities from external API. Now I’m able to change it and not only fetch data, but also save the data.

module App
  class SyncStravaActivities
    def initialize
      @strava_oauth_client = ::Strava::OAuth::Client.new(
        client_id: ENV["STRAVA_CLIENT_ID"],
        client_secret: ENV["STRAVA_CLIENT_SECRET"]
      )
    end

    def call
      update_tokens
      sync_all_activities
    end

    private

    def update_tokens
      response = @strava_oauth_client.oauth_token(
        refresh_token: Repositories::StravaIntegrations.new.get_refresh_token,
        grant_type: "refresh_token"
      )

      Repositories::StravaIntegrations.new.update_credentials(
        access_token: response.access_token, refresh_token: response.refresh_token
      )
    end

    def sync_all_activities
      client = Strava::Api::Client.new(access_token: Repositories::StravaIntegrations.new.get_access_token)

      page = 1

      loop do
        activities = client.athlete_activities(page: page)
        activities.each do |activity|
          next if Repositories::Activities.new.find_by_external_id(activity["id"])
          Repositories::Activities.new.create(
            distance: activity["distance"],
            time: activity["moving_time"],
            external_id: activity["id"],
            activity_date: activity["start_date"],
            name: activity["name"],
            commute: activity["commute"],
            sport_type: activity["sport_type"]
          )
        end
        break if activities.empty?

        page += 1
      end
    end
  end
end

Now comes the challenging part: calculating distances and times for bikes and components. Since components can have multiple assignments to bikes, I need to consider time slots. Here’s a simplified approach to calculate distances and times:

Title: Building a Flexible Bike Activity Tracking System

In the world of fitness and outdoor activities, there’s a growing need for flexible systems that can track various types of activities, whether it’s a bike ride, a virtual ride, or a daily commute. In this blog post, I’ll explore the development of a flexible activity tracking system that can integrate with different providers, store activity data, and calculate distances and times for bikes and components.

The Need for Flexibility When designing an activity tracking system, it’s essential to think ahead and plan for flexibility. The system should be able to integrate with different providers, such as Strava, and adapt to new providers in the future. To achieve this flexibility, I introduce a new entity called “Activity.” This entity will serve as a central point for storing activity data, allowing us to switch providers seamlessly.

The Entity: Activity To create the “Activity” entity, I start by defining its structure in the database. Here’s the migration to create the “activities” table:

class CreateActivities < ActiveRecord::Migration[7.0]
  def change
    create_table :activities do |t|
      t.float "distance"
      t.integer "time"
      t.string "external_id"
      t.datetime "activity_date"
      t.string "name"

      t.timestamps
    end
  end
end

I also define the “Activity” model and its associated repository to interact with the database.

Integration with Strava In our example, I focus on integrating with Strava, but keep in mind that the system’s design allows for easy integration with other providers. I use the Strava API to fetch activities, update access tokens, and store new activities. The “sync_activities” endpoint handles this integration.

Adding Flexibility to Activity Types Activities can vary, and they may be associated with different types of bikes and components. Strava provides data that helps us categorize activities into sport types and mark them as commutes. To add flexibility to our system, I introduce two new fields in the “bikes” table: “sport_type” and “commute.”

To maintain consistency, I update the “Bike” model and add default values for these fields in a new migration:

class AddDefaultTypesToBikes < ActiveRecord::Migration[7.0]
  def change
    add_column :bikes, :sport_type, :string
    add_column :bikes, :commute, :boolean, default: false
  end
end

Now comes the challenging part: calculating distances and times for bikes and components. Since components can have multiple assignments to bikes, I need to consider time slots. Here’s a simplified approach to calculate distances and times:

def calculate_distance(record)
  total_distance = 0
  record.component_assignments.each do |ca|
    activity_query = Records::Activity.where(commute: ca.bike.commute, sport_type: ca.bike.sport_type)
    activity_query = activity_query.where("activity_date >= ?", ca.started_at)
    activity_query = activity_query.where("activity_date <= ?", ca.ended_at) if ca.ended_at
    total_distance += activity_query.sum(:distance)
  end
  (total_distance / 1000).round(2).to_s + " KM"
end

def calculate_time(record)
  total_time = 0
  record.component_assignments each do |ca|
    activity_query = Records::Activity.where(commute: ca.bike.commute, sport_type: ca.bike.sport_type)
    activity_query = activity_query.where("activity_date >= ?", ca.started_at)
    activity_query = activity_query.where("activity_date <= ?", ca.ended_at) if ca.ended_at
    total_time += activity_query.sum(:time)
  end
  humanize(total_time)
end

This code loops through component assignments, considers time slots, and calculates the total distance and time for the given bike or component. The same code but for bikes is little simpler:

def calculate_distance(record)
        (Records::Activity.where(commute: record.commute, sport_type: record.sport_type).sum(:distance) / 1000).round(2).to_s + " KM"
      end

def calculate_time(record)
  seconds = Records::Activity.where(commute: record.commute, sport_type: record.sport_type).sum(:time)
  humanize(seconds)
end

In both cases there is method humanize which convert time to more readable form:

def humanize(secs)
  [[60, :seconds], [60, :minutes], [Float::INFINITY, :hours]].map { |count, name|
    if secs > 0
      secs, n = secs.divmod(count)

      "#{n.to_i} #{name}" unless n.to_i == 0
    end
  }.compact.reverse.join(" ")
end

While this system works, there’s room for improvement. For example, I could implement caching to make distance and time calculations more efficient, especially if the system deals with a large volume of data.

In conclusion, building a flexible activity tracking system that can adapt to different providers and handle various activity types is a complex but rewarding endeavor. By focusing on flexibility, data consistency, and efficient calculations, I can create a robust system that meets the evolving needs of my users.