content_signals 0.1.6 → 0.1.7
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 +4 -4
- data/lib/content_signals/configuration.rb +3 -1
- data/lib/content_signals/jobs/aggregate_analytics_job.rb +93 -0
- data/lib/content_signals/jobs/purge_page_views_job.rb +22 -0
- data/lib/content_signals/models/analytics_daily_stat.rb +39 -0
- data/lib/content_signals/version.rb +1 -1
- data/lib/content_signals.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c7724fb7019bc1a3ac2f2eb35edad5cd27cfb64e53c3c1f94c1ed9cb45fda40
|
|
4
|
+
data.tar.gz: 19e23f5b28e88ffdba2d8657e5aaa6664292cc87c170107a5ddd31d43e3e78a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dbe5d299d3266d8902fbbc0aee794f6b0ed2aafed261536745e91964318cb9bc25ad69c6cf2f05c620f658bcf5def00f03042b36b7b9967555ac471381181563
|
|
7
|
+
data.tar.gz: f3c14ea5307b6ff4bb39a4382de83783154af50ab27dd3ac90951204203d8361dae7ae47a5cd88f2cf304f98d41a37f4fabb2b76a66d72f7c5fa788af1b9ba64
|
|
@@ -11,7 +11,8 @@ module ContentSignals
|
|
|
11
11
|
:track_bots,
|
|
12
12
|
:track_admins,
|
|
13
13
|
:geoip_provider, # Symbol (:maxmind, :ipinfo, :null) or a provider class
|
|
14
|
-
:geoip_token
|
|
14
|
+
:geoip_token, # API token for online providers (e.g. IPinfo)
|
|
15
|
+
:retention_days # Days to keep raw page_views before purging (default: 90)
|
|
15
16
|
|
|
16
17
|
def initialize
|
|
17
18
|
@multitenancy = false
|
|
@@ -24,6 +25,7 @@ module ContentSignals
|
|
|
24
25
|
@track_admins = false
|
|
25
26
|
@geoip_provider = :maxmind
|
|
26
27
|
@geoip_token = nil
|
|
28
|
+
@retention_days = 90
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
# Returns the resolved provider class for IP geolocation.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ContentSignals
|
|
4
|
+
class AggregateAnalyticsJob < ActiveJob::Base
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
# Aggregates raw page_views for a given date into analytics_daily_stats.
|
|
8
|
+
# Designed to be run hourly — today's entry is upserted on each run.
|
|
9
|
+
# Pass date: as a Date or String (default: aggregates yesterday + today).
|
|
10
|
+
def perform(date: nil)
|
|
11
|
+
if date
|
|
12
|
+
aggregate_for_date(date.to_date)
|
|
13
|
+
else
|
|
14
|
+
aggregate_for_date(Date.yesterday)
|
|
15
|
+
aggregate_for_date(Date.today)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def aggregate_for_date(date)
|
|
22
|
+
day_scope = PageView.where(viewed_at: date.beginning_of_day..date.end_of_day)
|
|
23
|
+
return if day_scope.none?
|
|
24
|
+
|
|
25
|
+
groups = day_scope
|
|
26
|
+
.group(:tenant_id, :trackable_type, :trackable_id)
|
|
27
|
+
.count
|
|
28
|
+
|
|
29
|
+
groups.each_key do |(tenant_id, trackable_type, trackable_id)|
|
|
30
|
+
scope = day_scope.where(
|
|
31
|
+
tenant_id: tenant_id,
|
|
32
|
+
trackable_type: trackable_type,
|
|
33
|
+
trackable_id: trackable_id
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
row = {
|
|
37
|
+
tenant_id:,
|
|
38
|
+
trackable_type:,
|
|
39
|
+
trackable_id:,
|
|
40
|
+
date:,
|
|
41
|
+
total_views: scope.count,
|
|
42
|
+
unique_visitors: scope.distinct.count(:visitor_id),
|
|
43
|
+
app_views: scope.where.not(app_platform: [ nil, "" ]).count,
|
|
44
|
+
device_breakdown: breakdown(scope, :device_type),
|
|
45
|
+
os_breakdown: breakdown(scope, :os),
|
|
46
|
+
browser_breakdown: breakdown(scope, :browser),
|
|
47
|
+
country_breakdown: breakdown(scope, :country_name),
|
|
48
|
+
city_breakdown: breakdown(scope, :city),
|
|
49
|
+
referrer_breakdown: referrer_breakdown(scope),
|
|
50
|
+
locale_breakdown: breakdown(scope, :locale),
|
|
51
|
+
hourly_breakdown: hourly_breakdown(scope),
|
|
52
|
+
updated_at: Time.current
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
AnalyticsDailyStat.upsert(
|
|
56
|
+
row,
|
|
57
|
+
unique_by: %i[tenant_id trackable_type trackable_id date]
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def breakdown(scope, column)
|
|
63
|
+
scope.where.not(column => [ nil, "" ])
|
|
64
|
+
.group(column)
|
|
65
|
+
.count
|
|
66
|
+
.to_json
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def referrer_breakdown(scope)
|
|
70
|
+
scope.where.not(referrer: [ nil, "" ])
|
|
71
|
+
.group(:referrer)
|
|
72
|
+
.count
|
|
73
|
+
.each_with_object({}) { |(url, count), h|
|
|
74
|
+
domain = extract_domain(url)
|
|
75
|
+
h[domain] = (h[domain] || 0) + count
|
|
76
|
+
}
|
|
77
|
+
.to_json
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def hourly_breakdown(scope)
|
|
81
|
+
scope.group("CAST(EXTRACT(HOUR FROM viewed_at) AS INTEGER)")
|
|
82
|
+
.count
|
|
83
|
+
.transform_keys(&:to_s)
|
|
84
|
+
.to_json
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def extract_domain(url)
|
|
88
|
+
URI.parse(url).host&.sub(/\Awww\./, "") || url
|
|
89
|
+
rescue URI::InvalidURIError
|
|
90
|
+
url.to_s.truncate(60)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ContentSignals
|
|
4
|
+
class PurgePageViewsJob < ActiveJob::Base
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
# Deletes raw page_views older than retention_days (default 90).
|
|
8
|
+
# Run daily. Only deletes records that have already been aggregated
|
|
9
|
+
# (i.e., older than 1 day beyond retention window to be safe).
|
|
10
|
+
def perform
|
|
11
|
+
retention_days = ContentSignals.configuration.retention_days
|
|
12
|
+
cutoff = retention_days.days.ago.beginning_of_day
|
|
13
|
+
|
|
14
|
+
deleted = 0
|
|
15
|
+
PageView.where("viewed_at < ?", cutoff).in_batches(of: 2000) do |batch|
|
|
16
|
+
deleted += batch.delete_all
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Rails.logger.info "[ContentSignals] PurgePageViewsJob: deleted #{deleted} records older than #{cutoff.to_date}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ContentSignals
|
|
4
|
+
class AnalyticsDailyStat < ActiveRecord::Base
|
|
5
|
+
self.table_name = "content_signals_analytics_daily_stats"
|
|
6
|
+
|
|
7
|
+
validates :trackable_type, :trackable_id, :date, presence: true
|
|
8
|
+
|
|
9
|
+
# JSON breakdown columns — stored as text, parsed on read
|
|
10
|
+
BREAKDOWN_COLUMNS = %i[
|
|
11
|
+
device_breakdown
|
|
12
|
+
os_breakdown
|
|
13
|
+
browser_breakdown
|
|
14
|
+
country_breakdown
|
|
15
|
+
city_breakdown
|
|
16
|
+
referrer_breakdown
|
|
17
|
+
locale_breakdown
|
|
18
|
+
hourly_breakdown
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
BREAKDOWN_COLUMNS.each do |col|
|
|
22
|
+
define_method(col) do
|
|
23
|
+
raw = super()
|
|
24
|
+
return {} if raw.blank?
|
|
25
|
+
JSON.parse(raw)
|
|
26
|
+
rescue JSON::ParserError
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
define_method(:"#{col}=") do |value|
|
|
31
|
+
super(value.is_a?(Hash) ? value.to_json : value)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
scope :for_tenant, ->(id) { where(tenant_id: id) }
|
|
36
|
+
scope :for_trackable, ->(type, ids) { where(trackable_type: type, trackable_id: ids) }
|
|
37
|
+
scope :in_range, ->(range) { where(date: range) }
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/content_signals.rb
CHANGED
|
@@ -45,6 +45,7 @@ require_relative "content_signals/engine" if defined?(Rails::Engine)
|
|
|
45
45
|
# Always load models when ActiveRecord is available
|
|
46
46
|
if defined?(ActiveRecord)
|
|
47
47
|
require_relative "content_signals/models/page_view"
|
|
48
|
+
require_relative "content_signals/models/analytics_daily_stat"
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
# Load geoip providers
|
|
@@ -62,6 +63,8 @@ require_relative "content_signals/services/device_detector_service"
|
|
|
62
63
|
if defined?(ActiveJob)
|
|
63
64
|
require_relative "content_signals/jobs/track_page_view_job"
|
|
64
65
|
require_relative "content_signals/jobs/update_geoip_database_job"
|
|
66
|
+
require_relative "content_signals/jobs/aggregate_analytics_job"
|
|
67
|
+
require_relative "content_signals/jobs/purge_page_views_job"
|
|
65
68
|
end
|
|
66
69
|
|
|
67
70
|
# Load concerns if ActiveSupport is available
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: content_signals
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ionuț Munteanu Alexandru
|
|
@@ -120,8 +120,11 @@ files:
|
|
|
120
120
|
- lib/content_signals/geoip/ipinfo_provider.rb
|
|
121
121
|
- lib/content_signals/geoip/maxmind_provider.rb
|
|
122
122
|
- lib/content_signals/geoip/null_provider.rb
|
|
123
|
+
- lib/content_signals/jobs/aggregate_analytics_job.rb
|
|
124
|
+
- lib/content_signals/jobs/purge_page_views_job.rb
|
|
123
125
|
- lib/content_signals/jobs/track_page_view_job.rb
|
|
124
126
|
- lib/content_signals/jobs/update_geoip_database_job.rb
|
|
127
|
+
- lib/content_signals/models/analytics_daily_stat.rb
|
|
125
128
|
- lib/content_signals/models/page_view.rb
|
|
126
129
|
- lib/content_signals/services/device_detector_service.rb
|
|
127
130
|
- lib/content_signals/services/page_view_tracker.rb
|