footprinted 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9896445daea0ce69e019da7472258e3c87035ce60245e8941de245edda8d3865
4
+ data.tar.gz: 3b858b7cc413344388f9e9ccacce615e0a0c56a3e461d87cc69204e52c722b04
5
+ SHA512:
6
+ metadata.gz: 60f7ecfda0afd96cbdbc67591d1c6e5687e02bce4d30f8b5f2b3fdaad61d5493332b3d2a6c8183a0fc960d8e78dfc124af7e934cacdc77558ec5eb3f04e6c772
7
+ data.tar.gz: 876c40969d9a9de55dfe8298808dc162519da408f578e20d6f5b66516e9c272696f82c4f232027ec92915c23cb6277dbf1a49e95a18d6e3ee56c072bce378ef2
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
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
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
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.
data/README.md ADDED
@@ -0,0 +1,182 @@
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).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
data/TODO ADDED
@@ -0,0 +1,3 @@
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)
@@ -0,0 +1,10 @@
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
@@ -0,0 +1,73 @@
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
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,55 @@
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Footprinted
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,28 @@
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)
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,30 @@
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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ Footprinted.configure do |config|
4
+ end
@@ -0,0 +1,4 @@
1
+ module Footprinted
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: footprinted
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - rameerez
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-10-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: trackdown
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.7'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.7'
69
+ description: Track user activity with associated IP addresses and geolocation info,
70
+ easily and with minimal setup. It's good for tracking profile views, downloads,
71
+ login attempts, or any user interaction where location matters.
72
+ email:
73
+ - rubygems@rameerez.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - CHANGELOG.md
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - TODO
83
+ - lib/footprinted.rb
84
+ - lib/footprinted/configuration.rb
85
+ - lib/footprinted/model.rb
86
+ - lib/footprinted/railtie.rb
87
+ - lib/footprinted/trackable_activity.rb
88
+ - lib/footprinted/version.rb
89
+ - lib/generators/footprinted/install_generator.rb
90
+ - lib/generators/footprinted/templates/create_footprinted_trackable_activities.rb.erb
91
+ - lib/generators/footprinted/templates/footprinted.rb
92
+ - sig/footprinted.rbs
93
+ homepage: https://github.com/rameerez/footprinted
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ allowed_push_host: https://rubygems.org
98
+ homepage_uri: https://github.com/rameerez/footprinted
99
+ source_code_uri: https://github.com/rameerez/footprinted
100
+ changelog_uri: https://github.com/rameerez/footprinted/blob/main/CHANGELOG.md
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 3.0.0
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 3.5.16
117
+ signing_key:
118
+ specification_version: 4
119
+ summary: Track IP-geolocated user activity in your Rails app
120
+ test_files: []