diff --git a/README.md b/README.md
index 2cd19ea..7b722e1 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,48 @@ Check out a live demo at [linkarooie.com](https://linkarooie.com).
5. Visit `http://localhost:3000` in your browser.
+#### Analytics Temporary
+
+I have added analytics but not a dashboard to view them yet. This is how it works so far.
+
+- Open the Rails console `rails console`
+
+```ruby
+puts "Page Views for loftwah:"
+user = User.find_by(username: 'loftwah')
+puts user.page_views.count
+
+puts "\nMost recent Page Views with new data:"
+puts PageView.order(created_at: :desc).limit(5).map { |pv| "#{pv.path} - #{pv.created_at} - IP: #{pv.ip_address} - Session: #{pv.session_id}" }
+
+puts "\nLink Clicks for loftwah's links:"
+puts user.links.sum { |link| link.link_clicks.count }
+
+puts "\nMost recent Link Clicks with new data:"
+puts LinkClick.order(created_at: :desc).limit(5).map { |lc| "#{lc.link.title} - #{lc.created_at} - IP: #{lc.ip_address} - Session: #{lc.session_id}" }
+
+puts "\nMost viewed pages:"
+puts PageView.group(:path).order('count_id DESC').limit(5).count(:id)
+
+puts "\nMost clicked links:"
+puts LinkClick.joins(:link).group('links.title').order('count_id DESC').limit(5).count(:id)
+
+puts "\nTotal tracking counts:"
+puts "Page Views: #{PageView.count}"
+puts "Link Clicks: #{LinkClick.count}"
+puts "Achievement Views: #{AchievementView.count}"
+
+puts "\nUnique IP addresses:"
+puts "Page Views: #{PageView.distinct.count(:ip_address)}"
+puts "Link Clicks: #{LinkClick.distinct.count(:ip_address)}"
+puts "Achievement Views: #{AchievementView.distinct.count(:ip_address)}"
+
+puts "\nUnique sessions:"
+puts "Page Views: #{PageView.distinct.count(:session_id)}"
+puts "Link Clicks: #{LinkClick.distinct.count(:session_id)}"
+puts "Achievement Views: #{AchievementView.distinct.count(:session_id)}"
+```
+
### Docker Deployment
1. Build and start the Docker containers:
diff --git a/app/controllers/achievements_controller.rb b/app/controllers/achievements_controller.rb
index 35e1a85..f5ba036 100644
--- a/app/controllers/achievements_controller.rb
+++ b/app/controllers/achievements_controller.rb
@@ -7,6 +7,21 @@ def index
def show
@achievement = Achievement.find(params[:id])
+ AchievementView.create(
+ achievement: @achievement,
+ user: @achievement.user,
+ viewed_at: Time.current,
+ referrer: request.referrer,
+ browser: request.user_agent,
+ ip_address: request.ip,
+ session_id: request.session.id
+ )
+
+ if @achievement.url.present?
+ redirect_to @achievement.url, allow_other_host: true
+ else
+ render :show
+ end
end
def new
@@ -46,4 +61,4 @@ def destroy
def achievement_params
params.require(:achievement).permit(:title, :date, :description, :icon, :url)
end
-end
+end
\ No newline at end of file
diff --git a/app/controllers/links_controller.rb b/app/controllers/links_controller.rb
index 863c8b1..170d915 100644
--- a/app/controllers/links_controller.rb
+++ b/app/controllers/links_controller.rb
@@ -49,9 +49,23 @@ def user_links
@user.tags = JSON.parse(@user.tags) if @user.tags.is_a?(String)
end
+ def track_click
+ @link = Link.find(params[:id])
+ LinkClick.create(
+ link: @link,
+ user: @link.user,
+ clicked_at: Time.current,
+ referrer: request.referrer,
+ browser: request.user_agent,
+ ip_address: request.ip,
+ session_id: request.session.id
+ )
+ redirect_to @link.url, allow_other_host: true
+ end
+
private
def link_params
params.require(:link).permit(:url, :title, :description, :position, :icon, :visible, :pinned)
end
-end
+end
\ No newline at end of file
diff --git a/app/middleware/page_view_tracker.rb b/app/middleware/page_view_tracker.rb
new file mode 100644
index 0000000..4ca38b1
--- /dev/null
+++ b/app/middleware/page_view_tracker.rb
@@ -0,0 +1,35 @@
+class PageViewTracker
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new(env)
+ status, headers, response = @app.call(env)
+
+ if html_response?(headers) && !request.path.start_with?('/assets', '/rails/active_storage')
+ track_page_view(request)
+ end
+
+ [status, headers, response]
+ end
+
+ private
+
+ def html_response?(headers)
+ headers['Content-Type']&.include?('text/html')
+ end
+
+ def track_page_view(request)
+ user = User.find_by(username: request.path.split('/').last)
+ PageView.create(
+ user: user,
+ path: request.path,
+ referrer: request.referrer,
+ browser: request.user_agent,
+ visited_at: Time.current,
+ ip_address: request.ip,
+ session_id: request.session[:session_id]
+ ) if user
+ end
+end
\ No newline at end of file
diff --git a/app/models/achievement.rb b/app/models/achievement.rb
index cd70053..88e5a91 100644
--- a/app/models/achievement.rb
+++ b/app/models/achievement.rb
@@ -1,6 +1,8 @@
class Achievement < ApplicationRecord
belongs_to :user
+ has_many :achievement_views
+
validates :title, presence: true
validates :date, presence: true
validates :description, presence: true
diff --git a/app/models/achievement_view.rb b/app/models/achievement_view.rb
new file mode 100644
index 0000000..4876677
--- /dev/null
+++ b/app/models/achievement_view.rb
@@ -0,0 +1,4 @@
+class AchievementView < ApplicationRecord
+ belongs_to :achievement
+ belongs_to :user
+end
diff --git a/app/models/link.rb b/app/models/link.rb
index a3ffaca..5191d76 100644
--- a/app/models/link.rb
+++ b/app/models/link.rb
@@ -1,5 +1,7 @@
class Link < ApplicationRecord
belongs_to :user
+
+ has_many :link_clicks
scope :visible, -> { where(visible: true) }
scope :pinned, -> { where(pinned: true) }
diff --git a/app/models/link_click.rb b/app/models/link_click.rb
new file mode 100644
index 0000000..b53dadf
--- /dev/null
+++ b/app/models/link_click.rb
@@ -0,0 +1,4 @@
+class LinkClick < ApplicationRecord
+ belongs_to :link
+ belongs_to :user
+end
diff --git a/app/models/page_view.rb b/app/models/page_view.rb
new file mode 100644
index 0000000..34cc654
--- /dev/null
+++ b/app/models/page_view.rb
@@ -0,0 +1,3 @@
+class PageView < ApplicationRecord
+ belongs_to :user
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 9b373e1..b896b01 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -5,6 +5,10 @@ class User < ApplicationRecord
has_many :links, dependent: :destroy
has_many :achievements, dependent: :destroy
+ has_many :page_views
+ has_many :link_clicks
+ has_many :achievement_views
+
validates :username, presence: true, uniqueness: true
validates :full_name, presence: true
diff --git a/app/views/links/user_links.html.erb b/app/views/links/user_links.html.erb
index 4ab2761..1ebebca 100644
--- a/app/views/links/user_links.html.erb
+++ b/app/views/links/user_links.html.erb
@@ -26,7 +26,7 @@
<% @pinned_links.each do |link| %>
- <%= link_to link.url, target: "_blank", class: 'icon-link bg-gray-800 p-3 m-1' do %>
+ <%= link_to track_click_link_path(link), target: "_blank", class: 'icon-link bg-gray-800 p-3 m-1' do %>
<% end %>
<% end %>
@@ -44,7 +44,7 @@
<% @links.where(pinned: false).each do |link| %>
- <%= link_to link.url, target: "_blank", class: 'hover:underline' do %>
+ <%= link_to track_click_link_path(link), target: "_blank", class: 'hover:underline' do %>
<%= link.title %>
<% end %>
@@ -65,7 +65,7 @@
<% end %>
<% if achievement.url.present? %>
- <%= link_to achievement.title, achievement.url, class: 'text-lime-300 hover:underline', target: "_blank" %>
+ <%= link_to achievement.title, achievement_path(achievement), class: 'text-lime-300 hover:underline', target: "_blank" %>
<% else %>
<%= achievement.title %>
<% end %>
diff --git a/config/application.rb b/config/application.rb
index 8c7d03a..f28b3f7 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -1,6 +1,6 @@
require_relative "boot"
-
require "rails/all"
+require_relative "../app/middleware/page_view_tracker"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
@@ -16,6 +16,8 @@ class Application < Rails::Application
# Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w(assets tasks))
+ config.middleware.use PageViewTracker
+
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
@@ -24,4 +26,4 @@ class Application < Rails::Application
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
end
-end
+end
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 2dc0571..3ea34db 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -11,6 +11,12 @@
}
end
+ resources :links do
+ member do
+ get :track_click
+ end
+ end
+
# Other routes
get "up" => "rails/health#show", as: :rails_health_check
root to: 'pages#home'
diff --git a/db/migrate/20240818111049_create_page_views.rb b/db/migrate/20240818111049_create_page_views.rb
new file mode 100644
index 0000000..6885e1f
--- /dev/null
+++ b/db/migrate/20240818111049_create_page_views.rb
@@ -0,0 +1,13 @@
+class CreatePageViews < ActiveRecord::Migration[7.1]
+ def change
+ create_table :page_views do |t|
+ t.references :user, null: false, foreign_key: true
+ t.string :path
+ t.string :referrer
+ t.string :browser
+ t.datetime :visited_at
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240818111055_create_link_clicks.rb b/db/migrate/20240818111055_create_link_clicks.rb
new file mode 100644
index 0000000..b10af3a
--- /dev/null
+++ b/db/migrate/20240818111055_create_link_clicks.rb
@@ -0,0 +1,13 @@
+class CreateLinkClicks < ActiveRecord::Migration[7.1]
+ def change
+ create_table :link_clicks do |t|
+ t.references :link, null: false, foreign_key: true
+ t.references :user, null: false, foreign_key: true
+ t.datetime :clicked_at
+ t.string :referrer
+ t.string :browser
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240818111101_create_achievement_views.rb b/db/migrate/20240818111101_create_achievement_views.rb
new file mode 100644
index 0000000..3fd2820
--- /dev/null
+++ b/db/migrate/20240818111101_create_achievement_views.rb
@@ -0,0 +1,13 @@
+class CreateAchievementViews < ActiveRecord::Migration[7.1]
+ def change
+ create_table :achievement_views do |t|
+ t.references :achievement, null: false, foreign_key: true
+ t.references :user, null: false, foreign_key: true
+ t.datetime :viewed_at
+ t.string :referrer
+ t.string :browser
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240818123616_add_ip_address_and_session_id_to_achievement_views.rb b/db/migrate/20240818123616_add_ip_address_and_session_id_to_achievement_views.rb
new file mode 100644
index 0000000..97b8e01
--- /dev/null
+++ b/db/migrate/20240818123616_add_ip_address_and_session_id_to_achievement_views.rb
@@ -0,0 +1,6 @@
+class AddIpAddressAndSessionIdToAchievementViews < ActiveRecord::Migration[7.1]
+ def change
+ add_column :achievement_views, :ip_address, :string
+ add_column :achievement_views, :session_id, :string
+ end
+end
diff --git a/db/migrate/20240818123618_add_ip_address_and_session_id_to_link_clicks.rb b/db/migrate/20240818123618_add_ip_address_and_session_id_to_link_clicks.rb
new file mode 100644
index 0000000..5b6ae06
--- /dev/null
+++ b/db/migrate/20240818123618_add_ip_address_and_session_id_to_link_clicks.rb
@@ -0,0 +1,6 @@
+class AddIpAddressAndSessionIdToLinkClicks < ActiveRecord::Migration[7.1]
+ def change
+ add_column :link_clicks, :ip_address, :string
+ add_column :link_clicks, :session_id, :string
+ end
+end
diff --git a/db/migrate/20240818123619_add_ip_address_and_session_id_to_page_views.rb b/db/migrate/20240818123619_add_ip_address_and_session_id_to_page_views.rb
new file mode 100644
index 0000000..9885725
--- /dev/null
+++ b/db/migrate/20240818123619_add_ip_address_and_session_id_to_page_views.rb
@@ -0,0 +1,6 @@
+class AddIpAddressAndSessionIdToPageViews < ActiveRecord::Migration[7.1]
+ def change
+ add_column :page_views, :ip_address, :string
+ add_column :page_views, :session_id, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 4e53b62..84d297b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,21 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2024_07_26_063327) do
+ActiveRecord::Schema[7.1].define(version: 2024_08_18_123619) do
+ create_table "achievement_views", force: :cascade do |t|
+ t.integer "achievement_id", null: false
+ t.integer "user_id", null: false
+ t.datetime "viewed_at"
+ t.string "referrer"
+ t.string "browser"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "ip_address"
+ t.string "session_id"
+ t.index ["achievement_id"], name: "index_achievement_views_on_achievement_id"
+ t.index ["user_id"], name: "index_achievement_views_on_user_id"
+ end
+
create_table "achievements", force: :cascade do |t|
t.string "title"
t.date "date"
@@ -23,6 +37,48 @@
t.index ["user_id"], name: "index_achievements_on_user_id"
end
+ create_table "active_storage_attachments", force: :cascade do |t|
+ t.string "name", null: false
+ t.string "record_type", null: false
+ t.bigint "record_id", null: false
+ t.bigint "blob_id", null: false
+ t.datetime "created_at", null: false
+ t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
+ t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
+ end
+
+ create_table "active_storage_blobs", force: :cascade do |t|
+ t.string "key", null: false
+ t.string "filename", null: false
+ t.string "content_type"
+ t.text "metadata"
+ t.string "service_name", null: false
+ t.bigint "byte_size", null: false
+ t.string "checksum"
+ t.datetime "created_at", null: false
+ t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
+ end
+
+ create_table "active_storage_variant_records", force: :cascade do |t|
+ t.bigint "blob_id", null: false
+ t.string "variation_digest", null: false
+ t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
+ end
+
+ create_table "link_clicks", force: :cascade do |t|
+ t.integer "link_id", null: false
+ t.integer "user_id", null: false
+ t.datetime "clicked_at"
+ t.string "referrer"
+ t.string "browser"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "ip_address"
+ t.string "session_id"
+ t.index ["link_id"], name: "index_link_clicks_on_link_id"
+ t.index ["user_id"], name: "index_link_clicks_on_user_id"
+ end
+
create_table "links", force: :cascade do |t|
t.string "title"
t.string "url"
@@ -37,6 +93,19 @@
t.index ["user_id"], name: "index_links_on_user_id"
end
+ create_table "page_views", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.string "path"
+ t.string "referrer"
+ t.string "browser"
+ t.datetime "visited_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "ip_address"
+ t.string "session_id"
+ t.index ["user_id"], name: "index_page_views_on_user_id"
+ end
+
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -56,6 +125,13 @@
t.index ["username"], name: "index_users_on_username", unique: true
end
+ add_foreign_key "achievement_views", "achievements"
+ add_foreign_key "achievement_views", "users"
add_foreign_key "achievements", "users"
+ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
+ add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
+ add_foreign_key "link_clicks", "links"
+ add_foreign_key "link_clicks", "users"
add_foreign_key "links", "users"
+ add_foreign_key "page_views", "users"
end
diff --git a/spec/models/achievement_view_spec.rb b/spec/models/achievement_view_spec.rb
new file mode 100644
index 0000000..fea44af
--- /dev/null
+++ b/spec/models/achievement_view_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe AchievementView, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/models/link_click_spec.rb b/spec/models/link_click_spec.rb
new file mode 100644
index 0000000..39a7342
--- /dev/null
+++ b/spec/models/link_click_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe LinkClick, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/models/page_view_spec.rb b/spec/models/page_view_spec.rb
new file mode 100644
index 0000000..100ab4a
--- /dev/null
+++ b/spec/models/page_view_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe PageView, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end