Compare commits
15 Commits
9d1ff31c53
...
a895216bda
Author | SHA1 | Date | |
---|---|---|---|
a895216bda | |||
75cdd69b9b | |||
70f977cae1 | |||
444c3e67bc | |||
ffbd201d62 | |||
5bfa94bc82 | |||
f59955ca6e | |||
978cec359e | |||
6eca78da8d | |||
f918a42619 | |||
adb671e668 | |||
50321533f7 | |||
dd6cd0451d | |||
5f30e08a6e | |||
bf10e41c1e |
48
.github/workflows/docker-dev.yml
vendored
Normal file
48
.github/workflows/docker-dev.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: ${{ github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # 获取完整的 git history 以便生成正确的 tag
|
||||
-
|
||||
name: Get Version
|
||||
id: get_version
|
||||
run: |
|
||||
echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Login to ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{github.actor}}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:dev
|
@ -47,5 +47,5 @@ jobs:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest
|
||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:main
|
||||
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }}
|
2
Gemfile
2
Gemfile
@ -50,6 +50,8 @@ gem "kaminari", "~> 1.2"
|
||||
gem "meta-tags", "~> 2.22"
|
||||
gem "sitemap_generator", "~> 6.3"
|
||||
|
||||
gem "ahoy_matey", "~> 5.2"
|
||||
|
||||
# gem "whenever", "~> 1.0"
|
||||
gem "ruby-openai", "~> 7.3"
|
||||
gem "httparty", "~> 0.22.0"
|
||||
|
@ -84,6 +84,10 @@ GEM
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ahoy_matey (5.2.1)
|
||||
activesupport (>= 6.1)
|
||||
device_detector (>= 1)
|
||||
safely_block (>= 0.4)
|
||||
arbre (1.7.0)
|
||||
activesupport (>= 3.0.0)
|
||||
ruby2_keywords (>= 0.0.2)
|
||||
@ -136,6 +140,7 @@ GEM
|
||||
debug (1.10.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
device_detector (1.1.3)
|
||||
devise (4.9.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
@ -385,6 +390,7 @@ GEM
|
||||
rubyzip (2.4.1)
|
||||
rufus-scheduler (3.9.2)
|
||||
fugit (~> 1.1, >= 1.11.1)
|
||||
safely_block (0.4.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.28.0)
|
||||
base64 (~> 0.2)
|
||||
@ -485,6 +491,7 @@ PLATFORMS
|
||||
|
||||
DEPENDENCIES
|
||||
activeadmin (~> 3.2)
|
||||
ahoy_matey (~> 5.2)
|
||||
aws-sdk-s3 (~> 1.177)
|
||||
bootsnap
|
||||
brakeman
|
||||
|
31
app/admin/ahoy_events.rb
Normal file
31
app/admin/ahoy_events.rb
Normal file
@ -0,0 +1,31 @@
|
||||
ActiveAdmin.register Ahoy::Event do
|
||||
# See permitted parameters documentation:
|
||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||
#
|
||||
# Uncomment all parameters which should be permitted for assignment
|
||||
#
|
||||
# permit_params :visit_id, :user_id, :name, :properties, :time
|
||||
#
|
||||
# or
|
||||
#
|
||||
# permit_params do
|
||||
# permitted = [:visit_id, :user_id, :name, :properties, :time]
|
||||
# permitted << :other if params[:action] == 'create' && current_user.admin?
|
||||
# permitted
|
||||
# end
|
||||
menu priority: 101, label: "事件统计"
|
||||
|
||||
actions :index
|
||||
|
||||
index do
|
||||
column :id
|
||||
column :name
|
||||
column :time
|
||||
column :properties
|
||||
column :user_id
|
||||
end
|
||||
|
||||
filter :name
|
||||
filter :time
|
||||
filter :properties
|
||||
end
|
34
app/admin/ahoy_management.rb
Normal file
34
app/admin/ahoy_management.rb
Normal file
@ -0,0 +1,34 @@
|
||||
# app/admin/ahoy_management.rb
|
||||
ActiveAdmin.register_page "Ahoy Management" do
|
||||
menu label: "访问数据管理", parent: "系统管理"
|
||||
|
||||
content title: "访问数据管理" do
|
||||
columns do
|
||||
column do
|
||||
panel "数据统计" do
|
||||
attributes_table_for :ahoy do
|
||||
row("总事件数") { Ahoy::Event.count }
|
||||
row("总访问数") { Ahoy::Visit.count }
|
||||
row("最早事件") { Ahoy::Event.minimum(:time)&.strftime("%Y-%m-%d %H:%M:%S") }
|
||||
row("最早访问") { Ahoy::Visit.minimum(:started_at)&.strftime("%Y-%m-%d %H:%M:%S") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "操作" do
|
||||
div class: "buttons" do
|
||||
button_to "立即清理旧数据", admin_ahoy_management_cleanup_path, method: :post,
|
||||
data: { confirm: "确定要清理3个月前的数据吗?" },
|
||||
class: "button"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
page_action :cleanup, method: :post do
|
||||
CleanAhoyDataWorker.perform_async
|
||||
redirect_to admin_ahoy_management_path, notice: "清理任务已加入队列"
|
||||
end
|
||||
end
|
35
app/admin/ahoy_visits.rb
Normal file
35
app/admin/ahoy_visits.rb
Normal file
@ -0,0 +1,35 @@
|
||||
ActiveAdmin.register Ahoy::Visit do
|
||||
# See permitted parameters documentation:
|
||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||
#
|
||||
# Uncomment all parameters which should be permitted for assignment
|
||||
#
|
||||
# permit_params :visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at
|
||||
#
|
||||
# or
|
||||
#
|
||||
# permit_params do
|
||||
# permitted = [:visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at]
|
||||
# permitted << :other if params[:action] == 'create' && current_user.admin?
|
||||
# permitted
|
||||
# end
|
||||
|
||||
menu priority: 100, label: "访问统计"
|
||||
|
||||
actions :index
|
||||
|
||||
index do
|
||||
column :id
|
||||
column :visitor_token
|
||||
column :ip
|
||||
column :user_agent
|
||||
column :started_at
|
||||
column :city
|
||||
column :country
|
||||
column :region
|
||||
end
|
||||
|
||||
filter :started_at
|
||||
filter :city
|
||||
filter :country
|
||||
end
|
@ -11,6 +11,53 @@ ActiveAdmin.register_page "Dashboard" do
|
||||
end
|
||||
end
|
||||
|
||||
columns do
|
||||
column do
|
||||
panel "访问统计" do
|
||||
para "总访问量: #{Ahoy::Visit.count}"
|
||||
para "总事件数: #{Ahoy::Event.count}"
|
||||
para "独立访客数: #{Ahoy::Visit.distinct.count(:visitor_token)}"
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "热门城市" do
|
||||
table_for City.by_popularity.limit(10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "热门天气艺术" do
|
||||
table_for WeatherArt.by_popularity.limit(10) do
|
||||
column("作品") { |art| link_to(art.to_s, admin_weather_art_path(art)) }
|
||||
column("访问量") { |art| art.view_count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
column do
|
||||
panel "冷门活跃城市" do
|
||||
table_for City.least_popular_active.limit(10) do
|
||||
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
|
||||
column("访问量") { |city| city.view_count }
|
||||
# column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# 添加一个事件列表面板
|
||||
panel "最近事件" do
|
||||
table_for Ahoy::Event.order(time: :desc).limit(10) do
|
||||
column :time
|
||||
column :name
|
||||
column :properties
|
||||
end
|
||||
end
|
||||
|
||||
# Here is an example of a simple dashboard with columns and panels.
|
||||
#
|
||||
# columns do
|
||||
|
@ -1,8 +1,51 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include SeoConcern
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
# allow_browser versions: :modern
|
||||
allow_browser versions: :modern do |config|
|
||||
# 通用移动浏览器
|
||||
config.allow(/Mobile Safari/) # iOS Safari
|
||||
config.allow(/Chrome\/[\d.]+/) # Chrome 内核浏览器
|
||||
|
||||
# 国内主流浏览器
|
||||
config.allow(/Quark\/[\d.]+/) # 夸克浏览器
|
||||
config.allow(/HuaweiBrowser\/[\d.]+/) # 华为浏览器
|
||||
config.allow(/MiuiBrowser\/[\d.]+/) # 小米浏览器
|
||||
config.allow(/VivoBrowser\/[\d.]+/) # vivo浏览器
|
||||
config.allow(/OppoBrowser\/[\d.]+/) # OPPO浏览器
|
||||
config.allow(/UCBrowser\/[\d.]+/) # UC浏览器
|
||||
config.allow(/QQBrowser\/[\d.]+/) # QQ浏览器
|
||||
config.allow(/MicroMessenger\/[\d.]+/) # 微信内置浏览器
|
||||
config.allow(/Alipay/) # 支付宝内置浏览器
|
||||
config.allow(/BaiduBoxApp/) # 百度 App
|
||||
config.allow(/baiduboxapp/i) # 百度 App (小写)
|
||||
config.allow(/SogouMobile/) # 搜狗移动浏览器
|
||||
config.allow(/Weibo/) # 微博内置浏览器
|
||||
config.allow(/DingTalk/) # 钉钉内置浏览器
|
||||
config.allow(/ToutiaoMicroApp/) # 今日头条小程序
|
||||
config.allow(/BytedanceWebview/) # 字节跳动系浏览器
|
||||
config.allow(/ArkWeb/) # 鸿蒙浏览器
|
||||
|
||||
# 调试日志(开发环境)
|
||||
if Rails.env.development?
|
||||
config.on_failure do |browser|
|
||||
Rails.logger.info <<~INFO
|
||||
Browser blocked:
|
||||
Name: #{browser.name}
|
||||
Version: #{browser.version}
|
||||
User Agent: #{browser.ua}
|
||||
INFO
|
||||
end
|
||||
end
|
||||
end
|
||||
before_action :set_locale
|
||||
after_action :track_action
|
||||
|
||||
protected
|
||||
|
||||
def track_action
|
||||
ahoy.track "Viewed Application", request.path_parameters
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
class CitiesController < ApplicationController
|
||||
def index
|
||||
@regions = Region.includes(:countries).order(:name)
|
||||
@cities = City.includes(:country, country: :region).active.order(:name)
|
||||
@cities = City.includes(:country, country: :region).order(:name)
|
||||
|
||||
if params[:region]
|
||||
@current_region = Region.friendly.find(params[:region])
|
||||
@ -24,6 +24,11 @@ class CitiesController < ApplicationController
|
||||
|
||||
def show
|
||||
@city = City.friendly.find(params[:id])
|
||||
ahoy.track "View City", {
|
||||
city_id: @city.id,
|
||||
name: @city.name,
|
||||
event_type: "city_view"
|
||||
}
|
||||
|
||||
set_meta_tags(
|
||||
title: @city.name,
|
||||
|
@ -2,6 +2,18 @@ class WeatherArtsController < ApplicationController
|
||||
def show
|
||||
@city = City.friendly.find(params[:city_id])
|
||||
@weather_art = @city.weather_arts.friendly.find(params[:slug])
|
||||
|
||||
ahoy.track "View Weather Art", {
|
||||
weather_art_id: @weather_art.id,
|
||||
city_id: @weather_art.city_id,
|
||||
event_type: "weather_art_view"
|
||||
}
|
||||
ahoy.track "View City", {
|
||||
city_id: @city.id,
|
||||
name: @city.name,
|
||||
event_type: "city_view"
|
||||
}
|
||||
|
||||
set_meta_tags(
|
||||
title: "#{@city.name} Weather Art - #{@weather_art.weather_date.strftime('%B %d, %Y')}",
|
||||
description: "#{@city.name}'s weather visualized through AI art. #{@weather_art.description} at #{@weather_art.temperature}°C.",
|
||||
|
14
app/models/ahoy/event.rb
Normal file
14
app/models/ahoy/event.rb
Normal file
@ -0,0 +1,14 @@
|
||||
class Ahoy::Event < ApplicationRecord
|
||||
# include Ahoy::QueryMethods
|
||||
|
||||
self.table_name = "ahoy_events"
|
||||
|
||||
belongs_to :visit
|
||||
belongs_to :user, optional: true
|
||||
|
||||
serialize :properties, coder: JSON
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "id", "id_value", "name", "properties", "time", "user_id", "visit_id" ]
|
||||
end
|
||||
end
|
10
app/models/ahoy/visit.rb
Normal file
10
app/models/ahoy/visit.rb
Normal file
@ -0,0 +1,10 @@
|
||||
class Ahoy::Visit < ApplicationRecord
|
||||
self.table_name = "ahoy_visits"
|
||||
|
||||
has_many :events, class_name: "Ahoy::Event"
|
||||
belongs_to :user, optional: true
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "app_version", "browser", "city", "country", "device_type", "id", "ip", "landing_page", "latitude", "longitude", "os", "os_version", "platform", "referrer", "referring_domain", "region", "started_at", "user_agent", "user_id", "utm_campaign", "utm_content", "utm_medium", "utm_source", "utm_term", "visit_token", "visitor_token" ]
|
||||
end
|
||||
end
|
@ -5,6 +5,9 @@ class City < ApplicationRecord
|
||||
|
||||
has_many :weather_arts, dependent: :destroy
|
||||
|
||||
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :city_id
|
||||
has_many :events, class_name: "Ahoy::Event", foreign_key: :city_id
|
||||
|
||||
delegate :region, to: :country
|
||||
|
||||
validates :name, presence: true
|
||||
@ -17,6 +20,40 @@ class City < ApplicationRecord
|
||||
scope :by_country, ->(country_id) { where(country_id: country_id) }
|
||||
scope :active, -> { where(active: true) }
|
||||
|
||||
scope :by_popularity, -> {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
|
||||
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
else
|
||||
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
|
||||
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
end
|
||||
}
|
||||
|
||||
scope :least_popular_active, -> {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
active
|
||||
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
|
||||
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count ASC, cities.name ASC")
|
||||
else
|
||||
active
|
||||
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
|
||||
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
|
||||
.group("cities.id")
|
||||
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count ASC, cities.name ASC")
|
||||
end
|
||||
}
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
@ -65,4 +102,12 @@ class City < ApplicationRecord
|
||||
def latest_weather_art
|
||||
weather_arts.order(weather_date: :desc).first
|
||||
end
|
||||
|
||||
def view_count
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'city_view' AND json_extract(properties, '$.city_id') = ?", self.id).count
|
||||
else
|
||||
Ahoy::Event.where("properties::jsonb->>'event_type' = 'city_view' AND (properties::jsonb->>'city_id')::integer = ?", self.id).count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -5,9 +5,28 @@ class WeatherArt < ApplicationRecord
|
||||
belongs_to :city
|
||||
has_one_attached :image
|
||||
|
||||
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
|
||||
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
|
||||
|
||||
validates :weather_date, presence: true
|
||||
validates :city_id, presence: true
|
||||
|
||||
scope :by_popularity, -> {
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.weather_art_id') = weather_arts.id
|
||||
AND json_extract(ahoy_events.properties, '$.event_type') = 'weather_art_view'")
|
||||
.group("weather_arts.id")
|
||||
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
else
|
||||
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'weather_art_id')::integer = weather_arts.id
|
||||
AND ahoy_events.properties::jsonb->>'event_type' = 'weather_art_view'")
|
||||
.group("weather_arts.id")
|
||||
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
|
||||
.order("visit_count DESC")
|
||||
end
|
||||
}
|
||||
|
||||
def should_generate_new_friendly_id?
|
||||
weather_date_changed? || city_id_changed? || super
|
||||
end
|
||||
@ -23,4 +42,12 @@ class WeatherArt < ApplicationRecord
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
[ "city_id", "cloud", "created_at", "description", "feeling_temp", "humidity", "id", "id_value", "precipitation", "pressure", "prompt", "temperature", "updated_at", "visibility", "weather_date", "wind_scale", "wind_speed" ]
|
||||
end
|
||||
|
||||
def view_count
|
||||
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
||||
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'weather_art_view' AND json_extract(properties, '$.weather_art_id') = ?", self.id).count
|
||||
else
|
||||
Ahoy::Event.where("properties::jsonb->>'event_type' = 'weather_art_view' AND (properties::jsonb->>'weather_art_id')::integer = ?", self.id).count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -58,7 +58,7 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="text-center mt-12">
|
||||
<div class="text-center mt-12 mb-12">
|
||||
<%= link_to arts_path, class: "btn btn-primary btn-lg gap-2" do %>
|
||||
View All Weather Arts
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
@ -78,13 +78,13 @@
|
||||
<div class="container mx-auto flex flex-col gap-2">
|
||||
<div id="busuanzi_container" class="text-xs opacity-70">
|
||||
<div class="space-x-2">
|
||||
<span>Page Views: <span id="busuanzi_value_page_pv"></span></span>
|
||||
<span>Page Views: <span id="busuanzi_page_pv"></span></span>
|
||||
<span>|</span>
|
||||
<span>Page Visitors: <span id="busuanzi_value_page_uv"></span></span>
|
||||
<span>Page Visitors: <span id="busuanzi_page_uv"></span></span>
|
||||
<span>|</span>
|
||||
<span>Total Views: <span id="busuanzi_value_site_pv"></span></span>
|
||||
<span>Total Views: <span id="busuanzi_site_pv"></span></span>
|
||||
<span>|</span>
|
||||
<span>Total Visitors: <span id="busuanzi_value_site_uv"></span></span>
|
||||
<span>Total Visitors: <span id="busuanzi_site_uv"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
32
app/workers/clean_ahoy_data_worker.rb
Normal file
32
app/workers/clean_ahoy_data_worker.rb
Normal file
@ -0,0 +1,32 @@
|
||||
# app/workers/clean_ahoy_data_worker.rb
|
||||
class CleanAhoyDataWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: :default, retry: false
|
||||
|
||||
def perform
|
||||
cleanup_old_events
|
||||
cleanup_old_visits
|
||||
log_cleanup_results
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cleanup_old_events
|
||||
cutoff_date = 3.months.ago
|
||||
deleted_events_count = Ahoy::Event.where("time < ?", cutoff_date).delete_all
|
||||
Rails.logger.info "Deleted #{deleted_events_count} old Ahoy events"
|
||||
end
|
||||
|
||||
def cleanup_old_visits
|
||||
cutoff_date = 3.months.ago
|
||||
deleted_visits_count = Ahoy::Visit.where("started_at < ?", cutoff_date).delete_all
|
||||
Rails.logger.info "Deleted #{deleted_visits_count} old Ahoy visits"
|
||||
end
|
||||
|
||||
def log_cleanup_results
|
||||
Rails.logger.info "Ahoy cleanup completed at #{Time.current}"
|
||||
Rails.logger.info "Remaining events: #{Ahoy::Event.count}"
|
||||
Rails.logger.info "Remaining visits: #{Ahoy::Visit.count}"
|
||||
end
|
||||
end
|
14
config/initializers/ahoy.rb
Normal file
14
config/initializers/ahoy.rb
Normal file
@ -0,0 +1,14 @@
|
||||
class Ahoy::Store < Ahoy::DatabaseStore
|
||||
end
|
||||
|
||||
# set to true for JavaScript tracking
|
||||
Ahoy.api = true
|
||||
|
||||
# set to true for geocoding (and add the geocoder gem to your Gemfile)
|
||||
# we recommend configuring local geocoding as well
|
||||
# see https://github.com/ankane/ahoy#geocoding
|
||||
Ahoy.geocode = false
|
||||
|
||||
Ahoy.visit_duration = 30.minutes
|
||||
Ahoy.server_side_visits = :when_needed
|
||||
RETENTION_PERIOD = 3.months
|
@ -9,4 +9,10 @@ refresh_sitemap:
|
||||
class: RefreshSitemapWorker
|
||||
queue: default
|
||||
description: "Refresh sitemap daily"
|
||||
enabled: true
|
||||
|
||||
clean_ahoy_data:
|
||||
cron: '0 0 * * 0'
|
||||
class: CleanAhoyDataWorker
|
||||
queue: default
|
||||
enabled: true
|
61
db/migrate/20250126155239_create_ahoy_visits_and_events.rb
Normal file
61
db/migrate/20250126155239_create_ahoy_visits_and_events.rb
Normal file
@ -0,0 +1,61 @@
|
||||
class CreateAhoyVisitsAndEvents < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :ahoy_visits do |t|
|
||||
t.string :visit_token
|
||||
t.string :visitor_token
|
||||
|
||||
# the rest are recommended but optional
|
||||
# simply remove any you don't want
|
||||
|
||||
# user
|
||||
t.references :user
|
||||
|
||||
# standard
|
||||
t.string :ip
|
||||
t.text :user_agent
|
||||
t.text :referrer
|
||||
t.string :referring_domain
|
||||
t.text :landing_page
|
||||
|
||||
# technology
|
||||
t.string :browser
|
||||
t.string :os
|
||||
t.string :device_type
|
||||
|
||||
# location
|
||||
t.string :country
|
||||
t.string :region
|
||||
t.string :city
|
||||
t.float :latitude
|
||||
t.float :longitude
|
||||
|
||||
# utm parameters
|
||||
t.string :utm_source
|
||||
t.string :utm_medium
|
||||
t.string :utm_term
|
||||
t.string :utm_content
|
||||
t.string :utm_campaign
|
||||
|
||||
# native apps
|
||||
t.string :app_version
|
||||
t.string :os_version
|
||||
t.string :platform
|
||||
|
||||
t.datetime :started_at
|
||||
end
|
||||
|
||||
add_index :ahoy_visits, :visit_token, unique: true
|
||||
add_index :ahoy_visits, [ :visitor_token, :started_at ]
|
||||
|
||||
create_table :ahoy_events do |t|
|
||||
t.references :visit
|
||||
t.references :user
|
||||
|
||||
t.string :name
|
||||
t.text :properties
|
||||
t.datetime :time
|
||||
end
|
||||
|
||||
add_index :ahoy_events, [ :name, :time ]
|
||||
end
|
||||
end
|
44
db/schema.rb
generated
44
db/schema.rb
generated
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_01_23_155234) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_01_26_155239) do
|
||||
create_table "active_admin_comments", force: :cascade do |t|
|
||||
t.string "namespace"
|
||||
t.text "body"
|
||||
@ -65,6 +65,48 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_23_155234) do
|
||||
t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true
|
||||
end
|
||||
|
||||
create_table "ahoy_events", force: :cascade do |t|
|
||||
t.integer "visit_id"
|
||||
t.integer "user_id"
|
||||
t.string "name"
|
||||
t.text "properties"
|
||||
t.datetime "time"
|
||||
t.index ["name", "time"], name: "index_ahoy_events_on_name_and_time"
|
||||
t.index ["user_id"], name: "index_ahoy_events_on_user_id"
|
||||
t.index ["visit_id"], name: "index_ahoy_events_on_visit_id"
|
||||
end
|
||||
|
||||
create_table "ahoy_visits", force: :cascade do |t|
|
||||
t.string "visit_token"
|
||||
t.string "visitor_token"
|
||||
t.integer "user_id"
|
||||
t.string "ip"
|
||||
t.text "user_agent"
|
||||
t.text "referrer"
|
||||
t.string "referring_domain"
|
||||
t.text "landing_page"
|
||||
t.string "browser"
|
||||
t.string "os"
|
||||
t.string "device_type"
|
||||
t.string "country"
|
||||
t.string "region"
|
||||
t.string "city"
|
||||
t.float "latitude"
|
||||
t.float "longitude"
|
||||
t.string "utm_source"
|
||||
t.string "utm_medium"
|
||||
t.string "utm_term"
|
||||
t.string "utm_content"
|
||||
t.string "utm_campaign"
|
||||
t.string "app_version"
|
||||
t.string "os_version"
|
||||
t.string "platform"
|
||||
t.datetime "started_at"
|
||||
t.index ["user_id"], name: "index_ahoy_visits_on_user_id"
|
||||
t.index ["visit_token"], name: "index_ahoy_visits_on_visit_token", unique: true
|
||||
t.index ["visitor_token", "started_at"], name: "index_ahoy_visits_on_visitor_token_and_started_at"
|
||||
end
|
||||
|
||||
create_table "cities", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.float "latitude"
|
||||
|
Loading…
Reference in New Issue
Block a user