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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f129ecbd94abc76038d938efb823655c1aaffd4b1fc8b217c483ea86e9405626
4
- data.tar.gz: bba0405e60cdfb09fba1b8670fe4da9e8bcbb8845aa6e64447ab96cd7a1a5530
3
+ metadata.gz: 3c7724fb7019bc1a3ac2f2eb35edad5cd27cfb64e53c3c1f94c1ed9cb45fda40
4
+ data.tar.gz: 19e23f5b28e88ffdba2d8657e5aaa6664292cc87c170107a5ddd31d43e3e78a5
5
5
  SHA512:
6
- metadata.gz: 7c753478d857862451a8631ed95685041ca4cdbdb159e9b02c0b543cee99333afdebbaa73dda3bdf57a86a4129c9969a1b5784dc2777f236050091cfed29a7f4
7
- data.tar.gz: 96ee5adb3cdcca8182683d6ecc18733afdc5383e87f83581206f605ac52c4097ae2b23ac157a013506670a71ec6f9de24d85f7114d28256a86736a868ecf6361
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 # API token for online providers (e.g. IPinfo)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ContentSignals
4
- VERSION = "0.1.6"
4
+ VERSION = "0.1.7"
5
5
  end
@@ -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.6
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