├── Rakefile ├── lib ├── footprinted │ ├── version.rb │ ├── configuration.rb │ ├── railtie.rb │ ├── trackable_activity.rb │ └── model.rb ├── generators │ └── footprinted │ │ ├── templates │ │ ├── footprinted.rb │ │ └── create_footprinted_trackable_activities.rb.erb │ │ └── install_generator.rb └── footprinted.rb ├── .gitignore ├── sig └── footprinted.rbs ├── bin ├── setup └── console ├── Gemfile ├── CHANGELOG.md ├── TODO ├── LICENSE.txt ├── footprinted.gemspec ├── README.md └── Gemfile.lock /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | task default: %i[] 5 | -------------------------------------------------------------------------------- /lib/footprinted/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Footprinted 4 | VERSION = "0.1.0" 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/footprinted/templates/footprinted.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Footprinted.configure do |config| 4 | end 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /dist/ 10 | 11 | TODO -------------------------------------------------------------------------------- /sig/footprinted.rbs: -------------------------------------------------------------------------------- 1 | module Footprinted 2 | VERSION: String 3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 4 | end 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in footprinted.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | -------------------------------------------------------------------------------- /lib/footprinted/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Footprinted 4 | class Configuration 5 | 6 | def initialize 7 | # No configuration options yet 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.0] - 2024-09-25 4 | 5 | - Initial release 6 | - Added Trackable concern for easy activity tracking 7 | - Integrated with trackdown gem for IP geolocation 8 | - Added customizable tracking associations 9 | - Created install generator for easy setup 10 | - Added configuration options -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "footprinted" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /lib/footprinted/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Footprinted 4 | class Railtie < Rails::Railtie 5 | initializer "footprinted.initialize" do 6 | ActiveSupport.on_load(:active_record) do 7 | extend Footprinted::Model 8 | end 9 | end 10 | 11 | generators do 12 | require "generators/footprinted/install_generator" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | ✘ Add index on users t.index ["user_id"], name: "index_trackable_activities_on_user_id" @cancelled(24-09-26 16:59) 2 | ✔ make user:references optional in migration (activities may not have an associated user) -- or just make it more flexible, like a performer polymorphic association or something @done(24-10-30 01:35) 3 | ✔ can they have multiple activity types per model? (user: profile_views, downloads, etc) @done(24-09-26 17:28) -------------------------------------------------------------------------------- /lib/footprinted.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "footprinted/version" 4 | require_relative "footprinted/configuration" 5 | require_relative "footprinted/model" 6 | require_relative "footprinted/trackable_activity" 7 | 8 | module Footprinted 9 | class Error < StandardError; end 10 | 11 | class << self 12 | attr_writer :configuration 13 | end 14 | 15 | def self.configuration 16 | @configuration ||= Configuration.new 17 | end 18 | 19 | def self.configure 20 | yield(configuration) 21 | end 22 | 23 | def self.reset 24 | @configuration = Configuration.new 25 | end 26 | end 27 | 28 | require "footprinted/railtie" if defined?(Rails) 29 | -------------------------------------------------------------------------------- /lib/generators/footprinted/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/generators/base' 4 | require 'rails/generators/active_record' 5 | 6 | module Footprinted 7 | module Generators 8 | class InstallGenerator < Rails::Generators::Base 9 | include ActiveRecord::Generators::Migration 10 | 11 | source_root File.expand_path('templates', __dir__) 12 | 13 | def self.next_migration_number(dir) 14 | ActiveRecord::Generators::Base.next_migration_number(dir) 15 | end 16 | 17 | def create_migration_file 18 | migration_template 'create_footprinted_trackable_activities.rb.erb', File.join(db_migrate_path, "create_footprinted_trackable_activities.rb") 19 | end 20 | 21 | private 22 | 23 | def migration_version 24 | "[#{ActiveRecord::VERSION::STRING.to_f}]" 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Javi R 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/generators/footprinted/templates/create_footprinted_trackable_activities.rb.erb: -------------------------------------------------------------------------------- 1 | class CreateFootprintedTrackableActivities < ActiveRecord::Migration<%= migration_version %> 2 | def change 3 | primary_key_type, foreign_key_type = primary_and_foreign_key_types 4 | 5 | create_table :trackable_activities, id: primary_key_type do |t| 6 | t.inet :ip, null: false 7 | t.text :country 8 | t.text :city 9 | t.references :trackable, polymorphic: true, null: false, type: foreign_key_type, index: true 10 | t.references :performer, polymorphic: true, type: foreign_key_type, index: true 11 | t.text :activity_type, null: false 12 | 13 | t.timestamps 14 | end 15 | 16 | add_index :trackable_activities, [:trackable_type, :trackable_id, :activity_type] 17 | add_index :trackable_activities, :activity_type 18 | add_index :trackable_activities, :country 19 | end 20 | 21 | private 22 | 23 | def primary_and_foreign_key_types 24 | config = Rails.configuration.generators 25 | setting = config.options[config.orm][:primary_key_type] 26 | primary_key_type = setting || :primary_key 27 | foreign_key_type = setting || :bigint 28 | [primary_key_type, foreign_key_type] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /footprinted.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/footprinted/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "footprinted" 7 | spec.version = Footprinted::VERSION 8 | spec.authors = ["rameerez"] 9 | spec.email = ["rubygems@rameerez.com"] 10 | 11 | spec.summary = "Track IP-geolocated user activity in your Rails app" 12 | spec.description = "Track user activity with associated IP addresses and geolocation info, easily and with minimal setup. It's good for tracking profile views, downloads, login attempts, or any user interaction where location matters." 13 | spec.homepage = "https://github.com/rameerez/footprinted" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = ">= 3.0.0" 16 | 17 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 18 | 19 | spec.metadata["homepage_uri"] = spec.homepage 20 | spec.metadata["source_code_uri"] = "https://github.com/rameerez/footprinted" 21 | spec.metadata["changelog_uri"] = "https://github.com/rameerez/footprinted/blob/main/CHANGELOG.md" 22 | 23 | # Specify which files should be added to the gem when it is released. 24 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 25 | gemspec = File.basename(__FILE__) 26 | spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| 27 | ls.readlines("\x0", chomp: true).reject do |f| 28 | (f == gemspec) || 29 | f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile]) 30 | end 31 | end 32 | spec.bindir = "exe" 33 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 34 | spec.require_paths = ["lib"] 35 | 36 | spec.add_dependency "rails", ">= 7.0" 37 | spec.add_dependency "trackdown", "~> 0.1" 38 | 39 | spec.add_development_dependency "rspec", "~> 3.0" 40 | spec.add_development_dependency "rubocop", "~> 1.7" 41 | end 42 | -------------------------------------------------------------------------------- /lib/footprinted/trackable_activity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Footprinted 4 | class TrackableActivity < ActiveRecord::Base 5 | # Associations 6 | belongs_to :trackable, polymorphic: true 7 | belongs_to :performer, polymorphic: true, optional: true 8 | 9 | # Validations 10 | validates :ip, presence: true 11 | validates :activity_type, presence: true 12 | validates :trackable, presence: true 13 | 14 | # Callbacks 15 | before_save :set_geolocation_data 16 | 17 | # Scopes 18 | scope :by_activity, ->(type) { where(activity_type: type) } 19 | scope :by_country, ->(country) { where(country: country) } 20 | scope :recent, -> { order(created_at: :desc) } 21 | scope :performed_by, ->(performer) { where(performer: performer) } 22 | scope :between, ->(start_date, end_date) { where(created_at: start_date..end_date) } 23 | scope :last_days, ->(days) { where('created_at >= ?', days.days.ago) } 24 | 25 | # Class methods 26 | def self.activity_types 27 | distinct.pluck(:activity_type) 28 | end 29 | 30 | def self.countries 31 | distinct.where.not(country: nil).pluck(:country) 32 | end 33 | 34 | private 35 | 36 | def set_geolocation_data 37 | return unless ip.present? 38 | 39 | unless defined?(Trackdown) 40 | raise Footprinted::Error, "The Trackdown gem is not installed. Please add `gem 'trackdown'` to your Gemfile and follow the setup instructions to configure the gem and download an IP geolocation database." 41 | end 42 | 43 | unless Trackdown.database_exists? 44 | raise Footprinted::Error, "MaxMind IP geolocation database not found. Please follow the Trackdown gem setup instructions to configure the gem and download an IP geolocation database." 45 | end 46 | 47 | location = Trackdown.locate(ip.to_s) 48 | self.country = location.country_code 49 | self.city = location.city 50 | rescue => e 51 | Rails.logger.error "Failed to geolocate IP #{ip}: #{e.message}" 52 | nil # Don't fail the save if geolocation fails 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/footprinted/model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Footprinted 4 | module Model 5 | extend ActiveSupport::Concern 6 | 7 | # The Footprinted::Model module provides a flexible way to track activities related to any model that includes this concern. 8 | # 9 | # Footprinted tracks activities through a polymorphic association: the Footprinted::TrackableActivity model. 10 | # 11 | # The Footprinted::Model concern sets up a :trackable_activities association using the 12 | # Footprinted::TrackableActivity model. 13 | # 14 | # It also provides a basic method to track unnamed activities, `track_activity`, 15 | # which can be used as is (not recommended) or overridden with a custom activity type: 16 | # 17 | # Track specific types of activities using the `has_trackable` class method. 18 | # This method also dynamically defines a method to create activity records for the custom association. 19 | # For example, `has_trackable :profile_views` generates the `track_profile_view` method. 20 | # 21 | # Example: 22 | # class YourModel < ApplicationRecord 23 | # include Trackable 24 | # has_trackable :profile_views 25 | # end 26 | # 27 | # The above will: 28 | # - Create a `has_many :profile_views` association. 29 | # - Define a method `track_profile_view` (singular) to create records in `profile_views`. 30 | # 31 | # 32 | # Methods: 33 | # 34 | # - has_trackable(association_name): Sets up a custom association for tracking activities. 35 | # This method dynamically defines a tracking method based on the given association name. 36 | # 37 | # - track_activity(ip, user = nil): Default method provided to track activities. It logs 38 | # the IP address, and optionally, the user involved in the activity. This method can be 39 | # overridden in the model including this module for custom behavior. 40 | # 41 | # Note: 42 | # The Footprinted::TrackableActivity model must exist and have a polymorphic association set up 43 | # with the :trackable attribute for this concern to function correctly. 44 | 45 | included do 46 | has_many :trackable_activities, as: :trackable, class_name: 'Footprinted::TrackableActivity', dependent: :destroy 47 | end 48 | 49 | class_methods do 50 | # Method to set custom association names 51 | def has_trackable(association_name) 52 | track_method_name = "track_#{association_name.to_s.singularize}" 53 | 54 | has_many association_name, -> { where(activity_type: association_name.to_s.singularize) }, 55 | as: :trackable, class_name: 'Footprinted::TrackableActivity' 56 | 57 | # Define a custom method for tracking activities of this type 58 | define_method(track_method_name) do |ip:, performer: nil| 59 | send(association_name).create!( 60 | ip: ip, 61 | performer: performer, 62 | activity_type: association_name.to_s.singularize 63 | ) 64 | end 65 | end 66 | end 67 | 68 | # Fallback method for tracking activity. This will be overridden if has_trackable is called. 69 | def track_activity(ip:, user: nil, activity_type: nil) 70 | trackable_activities.create(ip: ip, user: user, activity_type: activity_type) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 👣 `footprinted` - Track geolocated user activity in Rails 2 | 3 | `footprinted` provides a simple way to track user activity with associated IP addresses and geolocation data in your Rails app. 4 | 5 | It's good for tracking profile views, downloads, login attempts, or any user interaction where location matters. 6 | 7 | ## Why 8 | 9 | Sometimes you need to know where your users are performing certain actions from. 10 | 11 | For example, let's say your users have profiles. Where has a particular profile been viewed from? 12 | 13 | This gem makes it trivial to track and analyze this kind of data: 14 | 15 | ```ruby 16 | # First, add this to your User model 17 | has_trackable :profile_views 18 | 19 | # Then, track the activity in the controller 20 | @user.track_profile_view(ip: request.remote_ip) 21 | 22 | # And finally, analyze the data 23 | @user.profile_views.group(:country).count 24 | # => { 'US'=>529, 'UK'=>291, 'CA'=>78... } 25 | ``` 26 | 27 | That's it! This is all you need for `footprinted` to store the profile view along with the IP's geolocation data. 28 | 29 | > [!NOTE] 30 | > By adding `has_trackable :profile_views` to your model, `footprinted` automatically creates a `profile_views` association and a `track_profile_view` method to your User model. 31 | > 32 | > `footprinted` does all the heavy lifting for you, so you don't need to define any models or associations. Just track and query. 33 | 34 | 35 | ## How it works 36 | 37 | `footprinted` relies on a `trackable_activities` table, and provides a model concern to interact with it. 38 | 39 | This model concern allows you to define polymorphic associations to store activity data associated with any model. 40 | 41 | For each activity, `footprinted` stores: 42 | - IP address 43 | - Country 44 | - City 45 | - Activity type 46 | - Event timestamp 47 | - Optionally, an associated `performer` record, which could be a `user`, `admin`, or any other model. It answers the question: "who triggered this activity?" 48 | 49 | `footprinted` also provides named methods that interact with the `trackable_activities` table to save and query this data. 50 | 51 | For example, `has_trackable :profile_views` will generate the `profile_views` association and the `track_profile_view` method. Similarly, `has_trackable :downloads` will generate the `downloads` association and the `track_download` method. 52 | 53 | ## Installation 54 | 55 | > [!IMPORTANT] 56 | > This gem depends on the [`trackdown`](https://github.com/rameerez/trackdown) gem for locating IPs. 57 | > 58 | > **Start by following the `trackdown` README to install and configure the gem**, and make sure you have a valid installation with a working MaxMind database before continuing – otherwise we won't be able to get any geolocation data from IPs. 59 | 60 | After [`trackdown`](https://github.com/rameerez/trackdown) has been installed and configured, add this line to your application's Gemfile: 61 | 62 | ```ruby 63 | gem 'footprinted' 64 | ``` 65 | 66 | And then execute: 67 | 68 | ```bash 69 | bundle install 70 | rails generate footprinted:install 71 | rails db:migrate 72 | ``` 73 | 74 | This will create a migration file to create the polymorphic `trackable_activities` table, and migrate the database. 75 | 76 | ## Usage 77 | 78 | ### Basic Setup 79 | 80 | Include the `Footprinted::Model` concern and declare what you want to track: 81 | 82 | ```ruby 83 | class User < ApplicationRecord 84 | include Footprinted::Model 85 | 86 | # Track a single activity type 87 | has_trackable :profile_views 88 | 89 | # Track multiple activity types 90 | has_trackable :downloads 91 | has_trackable :login_attempts 92 | end 93 | ``` 94 | 95 | ### Recording Activity 96 | 97 | `footprinted` generates methods for you. 98 | 99 | For example, the `has_trackable :profile_views` association automatically provides you with a `track_profile_view` method that you can use: 100 | 101 | ```ruby 102 | # Basic tracking with IP 103 | user.track_profile_view(ip: request.remote_ip) 104 | 105 | # Or track with a performer as well ("who triggered the activity?") 106 | user.track_profile_view( 107 | ip: request.remote_ip, 108 | performer: current_user 109 | ) 110 | ``` 111 | 112 | ### Querying Activity 113 | 114 | #### Basic Queries 115 | 116 | ```ruby 117 | # Basic queries 118 | user.profile_views.recent 119 | user.profile_views.last_days(7) 120 | user.profile_views.between(1.week.ago, Time.current) 121 | 122 | # Location queries 123 | user.profile_views.by_country('US') 124 | user.profile_views.countries # => ['US', 'UK', 'CA', ...] 125 | 126 | # Performer queries 127 | user.profile_views.performed_by(some_user) 128 | ``` 129 | 130 | ### Advanced Usage 131 | 132 | Track multiple activity types: 133 | 134 | ```ruby 135 | class Resource < ApplicationRecord 136 | include Footprinted::Model 137 | 138 | has_trackable :downloads 139 | has_trackable :previews 140 | end 141 | 142 | # Track activities 143 | product.track_download(ip: request.remote_ip) 144 | product.track_preview(ip: request.remote_ip) 145 | 146 | # Query activities 147 | product.downloads.count 148 | product.previews.last_days(30) 149 | product.downloads.between(1.week.ago, Time.current) 150 | ``` 151 | 152 | Time-based analysis: 153 | 154 | ```ruby 155 | # Daily activity for the last 30 days 156 | resource.downloads 157 | .where('created_at > ?', 30.days.ago) 158 | .group("DATE(created_at)") 159 | .count 160 | .transform_keys { |k| k.strftime("%Y-%m-%d") } 161 | # => {"2024-03-26" => 5, "2024-03-25" => 3, ...} 162 | 163 | # Hourly distribution 164 | resource.downloads 165 | .group("HOUR(created_at)") 166 | .count 167 | # => {0=>10, 1=>5, 2=>8, ...} 168 | ``` 169 | 170 | ## Development 171 | 172 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 173 | 174 | To install this gem onto your local machine, run `bundle exec rake install`. 175 | 176 | ## Contributing 177 | 178 | Bug reports and pull requests are welcome on GitHub at https://github.com/rameerez/footprinted. Our code of conduct is: just be nice and make your mom proud of what you do and post online. 179 | 180 | ## License 181 | 182 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 183 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | footprinted (0.1.0) 5 | rails (>= 7.0) 6 | trackdown (~> 0.1) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (7.2.1.2) 12 | actionpack (= 7.2.1.2) 13 | activesupport (= 7.2.1.2) 14 | nio4r (~> 2.0) 15 | websocket-driver (>= 0.6.1) 16 | zeitwerk (~> 2.6) 17 | actionmailbox (7.2.1.2) 18 | actionpack (= 7.2.1.2) 19 | activejob (= 7.2.1.2) 20 | activerecord (= 7.2.1.2) 21 | activestorage (= 7.2.1.2) 22 | activesupport (= 7.2.1.2) 23 | mail (>= 2.8.0) 24 | actionmailer (7.2.1.2) 25 | actionpack (= 7.2.1.2) 26 | actionview (= 7.2.1.2) 27 | activejob (= 7.2.1.2) 28 | activesupport (= 7.2.1.2) 29 | mail (>= 2.8.0) 30 | rails-dom-testing (~> 2.2) 31 | actionpack (7.2.1.2) 32 | actionview (= 7.2.1.2) 33 | activesupport (= 7.2.1.2) 34 | nokogiri (>= 1.8.5) 35 | racc 36 | rack (>= 2.2.4, < 3.2) 37 | rack-session (>= 1.0.1) 38 | rack-test (>= 0.6.3) 39 | rails-dom-testing (~> 2.2) 40 | rails-html-sanitizer (~> 1.6) 41 | useragent (~> 0.16) 42 | actiontext (7.2.1.2) 43 | actionpack (= 7.2.1.2) 44 | activerecord (= 7.2.1.2) 45 | activestorage (= 7.2.1.2) 46 | activesupport (= 7.2.1.2) 47 | globalid (>= 0.6.0) 48 | nokogiri (>= 1.8.5) 49 | actionview (7.2.1.2) 50 | activesupport (= 7.2.1.2) 51 | builder (~> 3.1) 52 | erubi (~> 1.11) 53 | rails-dom-testing (~> 2.2) 54 | rails-html-sanitizer (~> 1.6) 55 | activejob (7.2.1.2) 56 | activesupport (= 7.2.1.2) 57 | globalid (>= 0.3.6) 58 | activemodel (7.2.1.2) 59 | activesupport (= 7.2.1.2) 60 | activerecord (7.2.1.2) 61 | activemodel (= 7.2.1.2) 62 | activesupport (= 7.2.1.2) 63 | timeout (>= 0.4.0) 64 | activestorage (7.2.1.2) 65 | actionpack (= 7.2.1.2) 66 | activejob (= 7.2.1.2) 67 | activerecord (= 7.2.1.2) 68 | activesupport (= 7.2.1.2) 69 | marcel (~> 1.0) 70 | activesupport (7.2.1.2) 71 | base64 72 | bigdecimal 73 | concurrent-ruby (~> 1.0, >= 1.3.1) 74 | connection_pool (>= 2.2.5) 75 | drb 76 | i18n (>= 1.6, < 2) 77 | logger (>= 1.4.2) 78 | minitest (>= 5.1) 79 | securerandom (>= 0.3) 80 | tzinfo (~> 2.0, >= 2.0.5) 81 | ast (2.4.2) 82 | base64 (0.2.0) 83 | bigdecimal (3.1.8) 84 | builder (3.3.0) 85 | concurrent-ruby (1.3.4) 86 | connection_pool (2.4.1) 87 | countries (7.0.0) 88 | unaccent (~> 0.3) 89 | crass (1.0.6) 90 | date (3.3.4) 91 | diff-lcs (1.5.1) 92 | drb (2.2.1) 93 | erubi (1.13.0) 94 | globalid (1.2.1) 95 | activesupport (>= 6.1) 96 | i18n (1.14.6) 97 | concurrent-ruby (~> 1.0) 98 | io-console (0.7.2) 99 | irb (1.14.1) 100 | rdoc (>= 4.0.0) 101 | reline (>= 0.4.2) 102 | json (2.7.4) 103 | language_server-protocol (3.17.0.3) 104 | logger (1.6.1) 105 | loofah (2.23.1) 106 | crass (~> 1.0.2) 107 | nokogiri (>= 1.12.0) 108 | mail (2.8.1) 109 | mini_mime (>= 0.1.1) 110 | net-imap 111 | net-pop 112 | net-smtp 113 | marcel (1.0.4) 114 | maxmind-db (1.2.0) 115 | mini_mime (1.1.5) 116 | minitest (5.25.1) 117 | net-imap (0.5.0) 118 | date 119 | net-protocol 120 | net-pop (0.1.2) 121 | net-protocol 122 | net-protocol (0.2.2) 123 | timeout 124 | net-smtp (0.5.0) 125 | net-protocol 126 | nio4r (2.7.4) 127 | nokogiri (1.16.7-aarch64-linux) 128 | racc (~> 1.4) 129 | nokogiri (1.16.7-arm-linux) 130 | racc (~> 1.4) 131 | nokogiri (1.16.7-arm64-darwin) 132 | racc (~> 1.4) 133 | nokogiri (1.16.7-x86-linux) 134 | racc (~> 1.4) 135 | nokogiri (1.16.7-x86_64-darwin) 136 | racc (~> 1.4) 137 | nokogiri (1.16.7-x86_64-linux) 138 | racc (~> 1.4) 139 | parallel (1.26.3) 140 | parser (3.3.5.0) 141 | ast (~> 2.4.1) 142 | racc 143 | psych (5.1.2) 144 | stringio 145 | racc (1.8.1) 146 | rack (3.1.8) 147 | rack-session (2.0.0) 148 | rack (>= 3.0.0) 149 | rack-test (2.1.0) 150 | rack (>= 1.3) 151 | rackup (2.1.0) 152 | rack (>= 3) 153 | webrick (~> 1.8) 154 | rails (7.2.1.2) 155 | actioncable (= 7.2.1.2) 156 | actionmailbox (= 7.2.1.2) 157 | actionmailer (= 7.2.1.2) 158 | actionpack (= 7.2.1.2) 159 | actiontext (= 7.2.1.2) 160 | actionview (= 7.2.1.2) 161 | activejob (= 7.2.1.2) 162 | activemodel (= 7.2.1.2) 163 | activerecord (= 7.2.1.2) 164 | activestorage (= 7.2.1.2) 165 | activesupport (= 7.2.1.2) 166 | bundler (>= 1.15.0) 167 | railties (= 7.2.1.2) 168 | rails-dom-testing (2.2.0) 169 | activesupport (>= 5.0.0) 170 | minitest 171 | nokogiri (>= 1.6) 172 | rails-html-sanitizer (1.6.0) 173 | loofah (~> 2.21) 174 | nokogiri (~> 1.14) 175 | railties (7.2.1.2) 176 | actionpack (= 7.2.1.2) 177 | activesupport (= 7.2.1.2) 178 | irb (~> 1.13) 179 | rackup (>= 1.0.0) 180 | rake (>= 12.2) 181 | thor (~> 1.0, >= 1.2.2) 182 | zeitwerk (~> 2.6) 183 | rainbow (3.1.1) 184 | rake (13.2.1) 185 | rdoc (6.7.0) 186 | psych (>= 4.0.0) 187 | regexp_parser (2.9.2) 188 | reline (0.5.10) 189 | io-console (~> 0.5) 190 | rspec (3.13.0) 191 | rspec-core (~> 3.13.0) 192 | rspec-expectations (~> 3.13.0) 193 | rspec-mocks (~> 3.13.0) 194 | rspec-core (3.13.2) 195 | rspec-support (~> 3.13.0) 196 | rspec-expectations (3.13.3) 197 | diff-lcs (>= 1.2.0, < 2.0) 198 | rspec-support (~> 3.13.0) 199 | rspec-mocks (3.13.2) 200 | diff-lcs (>= 1.2.0, < 2.0) 201 | rspec-support (~> 3.13.0) 202 | rspec-support (3.13.1) 203 | rubocop (1.67.0) 204 | json (~> 2.3) 205 | language_server-protocol (>= 3.17.0) 206 | parallel (~> 1.10) 207 | parser (>= 3.3.0.2) 208 | rainbow (>= 2.2.2, < 4.0) 209 | regexp_parser (>= 2.4, < 3.0) 210 | rubocop-ast (>= 1.32.2, < 2.0) 211 | ruby-progressbar (~> 1.7) 212 | unicode-display_width (>= 2.4.0, < 3.0) 213 | rubocop-ast (1.33.0) 214 | parser (>= 3.3.1.0) 215 | ruby-progressbar (1.13.0) 216 | securerandom (0.3.1) 217 | stringio (3.1.1) 218 | thor (1.3.2) 219 | timeout (0.4.1) 220 | trackdown (0.1.1) 221 | connection_pool (~> 2.4) 222 | countries (~> 7.0) 223 | maxmind-db (~> 1.2) 224 | tzinfo (2.0.6) 225 | concurrent-ruby (~> 1.0) 226 | unaccent (0.4.0) 227 | unicode-display_width (2.6.0) 228 | useragent (0.16.10) 229 | webrick (1.8.2) 230 | websocket-driver (0.7.6) 231 | websocket-extensions (>= 0.1.0) 232 | websocket-extensions (0.1.5) 233 | zeitwerk (2.7.1) 234 | 235 | PLATFORMS 236 | aarch64-linux 237 | arm-linux 238 | arm64-darwin 239 | x86-linux 240 | x86_64-darwin 241 | x86_64-linux 242 | 243 | DEPENDENCIES 244 | footprinted! 245 | rake (~> 13.0) 246 | rspec (~> 3.0) 247 | rubocop (~> 1.7) 248 | 249 | BUNDLED WITH 250 | 2.5.17 251 | --------------------------------------------------------------------------------