content_signals 0.1.1 β 0.1.2
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/README.md +15 -9
- data/lib/content_signals/configuration.rb +19 -1
- data/lib/content_signals/geoip/base_provider.rb +26 -0
- data/lib/content_signals/geoip/ipinfo_provider.rb +50 -0
- data/lib/content_signals/geoip/maxmind_provider.rb +41 -0
- data/lib/content_signals/geoip/null_provider.rb +13 -0
- data/lib/content_signals/jobs/update_geoip_database_job.rb +61 -0
- data/lib/content_signals/services/page_view_tracker.rb +10 -0
- data/lib/content_signals/services/visitor_location_service.rb +5 -69
- data/lib/content_signals/version.rb +1 -1
- data/lib/content_signals.rb +13 -3
- data/lib/generators/content_signals/install_generator.rb +34 -6
- data/lib/generators/content_signals/templates/content_signals.rb.erb +15 -3
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7388e4fede60244368fbc17faba350e4427c9a028a7c4482b526836bc0f9f5ac
|
|
4
|
+
data.tar.gz: 14969ae023b16f9ca1a5db5922abddddf35444b860d2aa0c86211f9c6ebf695b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c020e32d9d0f675d11873f5933cef9b100944a9067df91956f05015eedb55b1a5ffe36a1f3337b3033093f0cabdd27547507eacdfc006d3abcbadddc7517152d
|
|
7
|
+
data.tar.gz: 02ce4de9492392cf993858d3f1d0a6b8713c915766357580bbda9c512f96df65c08e580bd8d470b6f2e23e81e83a5b86dab55156ae8f50a5ca518ca8ebf1dbe3
|
data/README.md
CHANGED
|
@@ -6,15 +6,21 @@ A Rails engine for tracking page views and content engagement with rich analytic
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
9
|
+
- **Page view tracking** with visitor identification
|
|
10
|
+
- **Use cookies** to identify anonymous returning visitors
|
|
11
|
+
- **Use Redis to deduplicate unique views per day** (optional)
|
|
12
|
+
- **Device detection** (mobile, tablet, desktop, hybrid apps)
|
|
13
|
+
- **Bot filtering** (automatically excludes crawlers)
|
|
14
|
+
- **Background processing** (non-blocking with ActiveJob)
|
|
15
|
+
- **Multi-tenant support** (optional)
|
|
16
|
+
- **Rich analytics** with time-based scopes and aggregations
|
|
17
|
+
- **Polymorphic tracking** (works with any model)
|
|
18
|
+
- **Hybrid app support** (Capacitor, Cordova, React Native, Flutter)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Further development plans
|
|
22
|
+
- **Geolocation** (country, city, region) via MaxMind GeoLite2
|
|
23
|
+
|
|
18
24
|
|
|
19
25
|
## Installation
|
|
20
26
|
|
|
@@ -9,7 +9,9 @@ module ContentSignals
|
|
|
9
9
|
:redis_namespace,
|
|
10
10
|
:maxmind_db_path,
|
|
11
11
|
:track_bots,
|
|
12
|
-
:track_admins
|
|
12
|
+
:track_admins,
|
|
13
|
+
:geoip_provider, # Symbol (:maxmind, :ipinfo, :null) or a provider class
|
|
14
|
+
:geoip_token # API token for online providers (e.g. IPinfo)
|
|
13
15
|
|
|
14
16
|
def initialize
|
|
15
17
|
@multitenancy = false
|
|
@@ -20,6 +22,22 @@ module ContentSignals
|
|
|
20
22
|
@maxmind_db_path = default_maxmind_path
|
|
21
23
|
@track_bots = false
|
|
22
24
|
@track_admins = false
|
|
25
|
+
@geoip_provider = :maxmind
|
|
26
|
+
@geoip_token = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the resolved provider class for IP geolocation.
|
|
30
|
+
# Accepts a symbol (:maxmind, :ipinfo, :null) or any class that responds to .locate(ip).
|
|
31
|
+
def resolved_geoip_provider
|
|
32
|
+
case @geoip_provider
|
|
33
|
+
when :maxmind then ContentSignals::Geoip::MaxmindProvider
|
|
34
|
+
when :ipinfo then ContentSignals::Geoip::IpinfoProvider
|
|
35
|
+
when :null then ContentSignals::Geoip::NullProvider
|
|
36
|
+
when Class then @geoip_provider
|
|
37
|
+
else
|
|
38
|
+
raise ArgumentError, "Unknown geoip_provider: #{@geoip_provider.inspect}. " \
|
|
39
|
+
"Use :maxmind, :ipinfo, :null, or a custom provider class."
|
|
40
|
+
end
|
|
23
41
|
end
|
|
24
42
|
|
|
25
43
|
def multitenancy?
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ContentSignals
|
|
4
|
+
module Geoip
|
|
5
|
+
# Base interface for IP geolocation providers.
|
|
6
|
+
# All providers must implement `.locate(ip_address)` and return either:
|
|
7
|
+
# - A Hash with keys: :country_code, :country_name, :city, :region, :latitude, :longitude
|
|
8
|
+
# - nil (when IP is local, lookup fails, or provider is disabled)
|
|
9
|
+
class BaseProvider
|
|
10
|
+
LOCAL_IP_PREFIXES = %w[127. 192.168. 10. 172. fc00: fe80:].freeze
|
|
11
|
+
LOCAL_IP_EXACT = %w[::1 localhost].freeze
|
|
12
|
+
|
|
13
|
+
def self.locate(ip_address)
|
|
14
|
+
raise NotImplementedError, "#{name}.locate must be implemented"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.local_ip?(ip)
|
|
18
|
+
return true if ip.nil?
|
|
19
|
+
|
|
20
|
+
ip_str = ip.to_s
|
|
21
|
+
LOCAL_IP_EXACT.include?(ip_str) ||
|
|
22
|
+
LOCAL_IP_PREFIXES.any? { |prefix| ip_str.start_with?(prefix) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ContentSignals
|
|
4
|
+
module Geoip
|
|
5
|
+
# Online geolocation via IPinfo.io API.
|
|
6
|
+
# Requires: IPINFO_TOKEN env var (or config.geoip_token)
|
|
7
|
+
# Requires the http gem (gem "http") or net/http (built-in, used as fallback).
|
|
8
|
+
class IpinfoProvider < BaseProvider
|
|
9
|
+
API_BASE = "https://ipinfo.io"
|
|
10
|
+
|
|
11
|
+
def self.locate(ip_address)
|
|
12
|
+
return nil if ip_address.blank? || local_ip?(ip_address)
|
|
13
|
+
|
|
14
|
+
token = ContentSignals.configuration.geoip_token || ENV["IPINFO_TOKEN"]
|
|
15
|
+
return nil unless token.present?
|
|
16
|
+
|
|
17
|
+
data = fetch(ip_address, token)
|
|
18
|
+
return nil unless data && data["country"]
|
|
19
|
+
|
|
20
|
+
lat, lng = data["loc"]&.split(",")&.map(&:to_f)
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
country_code: data["country"],
|
|
24
|
+
country_name: country_name(data["country"]),
|
|
25
|
+
city: data["city"],
|
|
26
|
+
region: data["region"],
|
|
27
|
+
latitude: lat,
|
|
28
|
+
longitude: lng
|
|
29
|
+
}
|
|
30
|
+
rescue => e
|
|
31
|
+
Rails.logger.error "IPinfo geolocation error: #{e.message}"
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.fetch(ip, token)
|
|
36
|
+
uri = URI("#{API_BASE}/#{ip}/json?token=#{token}")
|
|
37
|
+
response = Net::HTTP.get_response(uri)
|
|
38
|
+
JSON.parse(response.body)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.country_name(code)
|
|
42
|
+
return nil unless defined?(ISO3166)
|
|
43
|
+
|
|
44
|
+
ISO3166::Country[code]&.name
|
|
45
|
+
rescue
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ContentSignals
|
|
4
|
+
module Geoip
|
|
5
|
+
# Offline geolocation using a local MaxMind GeoLite2-City .mmdb file.
|
|
6
|
+
# Requires the maxminddb gem.
|
|
7
|
+
# Download/update the database: bundle exec rake app:stejar:geoip:update
|
|
8
|
+
class MaxmindProvider < BaseProvider
|
|
9
|
+
def self.locate(ip_address)
|
|
10
|
+
return nil if ip_address.blank? || local_ip?(ip_address)
|
|
11
|
+
|
|
12
|
+
db_path = ContentSignals.configuration.maxmind_db_path
|
|
13
|
+
return nil unless db_path && File.exist?(db_path.to_s)
|
|
14
|
+
|
|
15
|
+
db = @db ||= MaxMindDB.new(db_path.to_s)
|
|
16
|
+
result = db.lookup(ip_address)
|
|
17
|
+
return nil unless result&.found?
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
country_code: result.country.iso_code,
|
|
21
|
+
country_name: result.country.name,
|
|
22
|
+
city: result.city.name,
|
|
23
|
+
region: result.subdivisions.most_specific&.name,
|
|
24
|
+
latitude: result.location.latitude,
|
|
25
|
+
longitude: result.location.longitude
|
|
26
|
+
}
|
|
27
|
+
rescue MaxMindDB::Error => e
|
|
28
|
+
Rails.logger.error "MaxMind lookup error: #{e.message}"
|
|
29
|
+
nil
|
|
30
|
+
rescue => e
|
|
31
|
+
Rails.logger.error "MaxMind geolocation error: #{e.message}"
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Reset cached DB handle (useful after db file is updated)
|
|
36
|
+
def self.reset!
|
|
37
|
+
@db = nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ContentSignals
|
|
4
|
+
module Geoip
|
|
5
|
+
# No-op provider β disables geolocation entirely.
|
|
6
|
+
# Use when you don't want any IP lookup performed.
|
|
7
|
+
class NullProvider < BaseProvider
|
|
8
|
+
def self.locate(_ip_address)
|
|
9
|
+
nil
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ContentSignals
|
|
4
|
+
# Downloads/refreshes the MaxMind GeoLite2-City database from the public mirror.
|
|
5
|
+
# Only runs when geoip_provider is :maxmind and maxmind_db_path is configured.
|
|
6
|
+
# Schedule via config/recurring.yml (added by `rails generate content_signals:install`).
|
|
7
|
+
class UpdateGeoipDatabaseJob < ApplicationJob
|
|
8
|
+
queue_as :default
|
|
9
|
+
|
|
10
|
+
MIRROR_URL = "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb"
|
|
11
|
+
|
|
12
|
+
def perform
|
|
13
|
+
config = ContentSignals.configuration
|
|
14
|
+
|
|
15
|
+
unless config.geoip_provider == :maxmind
|
|
16
|
+
Rails.logger.info "[ContentSignals] UpdateGeoipDatabaseJob skipped: geoip_provider is not :maxmind"
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
db_path = config.maxmind_db_path
|
|
21
|
+
unless db_path.present?
|
|
22
|
+
Rails.logger.warn "[ContentSignals] UpdateGeoipDatabaseJob skipped: maxmind_db_path not configured"
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
download(db_path.to_s)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def download(db_path)
|
|
32
|
+
require "open-uri"
|
|
33
|
+
require "fileutils"
|
|
34
|
+
|
|
35
|
+
FileUtils.mkdir_p(File.dirname(db_path))
|
|
36
|
+
|
|
37
|
+
tmp_path = "#{db_path}.tmp"
|
|
38
|
+
|
|
39
|
+
Rails.logger.info "[ContentSignals] Downloading GeoLite2-City from mirror..."
|
|
40
|
+
|
|
41
|
+
URI.open(MIRROR_URL, "rb", # rubocop:disable Security/Open
|
|
42
|
+
"User-Agent" => "Mozilla/5.0",
|
|
43
|
+
read_timeout: 120,
|
|
44
|
+
open_timeout: 15) do |remote|
|
|
45
|
+
File.binwrite(tmp_path, remote.read)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
File.rename(tmp_path, db_path)
|
|
49
|
+
|
|
50
|
+
size_mb = (File.size(db_path) / 1_048_576.0).round(1)
|
|
51
|
+
Rails.logger.info "[ContentSignals] GeoLite2-City updated: #{db_path} (#{size_mb} MB)"
|
|
52
|
+
|
|
53
|
+
# Bust the cached DB handle so next lookup uses the fresh file
|
|
54
|
+
ContentSignals::Geoip::MaxmindProvider.reset!
|
|
55
|
+
rescue => e
|
|
56
|
+
File.delete(tmp_path) if tmp_path && File.exist?(tmp_path)
|
|
57
|
+
Rails.logger.error "[ContentSignals] GeoLite2 update failed: #{e.message}"
|
|
58
|
+
raise
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -44,6 +44,16 @@ module ContentSignals
|
|
|
44
44
|
if @request.respond_to?(:cookie_jar)
|
|
45
45
|
cookie_id = @request.cookie_jar.signed[:visitor_id]
|
|
46
46
|
return cookie_id if cookie_id.present?
|
|
47
|
+
|
|
48
|
+
# Generate and set new visitor cookie if none exists
|
|
49
|
+
new_visitor_id = "visitor_#{SecureRandom.uuid}"
|
|
50
|
+
@request.cookie_jar.signed[:visitor_id] = {
|
|
51
|
+
value: new_visitor_id,
|
|
52
|
+
expires: 2.years.from_now,
|
|
53
|
+
httponly: true,
|
|
54
|
+
same_site: :lax
|
|
55
|
+
}
|
|
56
|
+
return new_visitor_id
|
|
47
57
|
end
|
|
48
58
|
|
|
49
59
|
# 4. Fallback to IP + User Agent hash (for non-cookie scenarios)
|
|
@@ -1,80 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ContentSignals
|
|
4
|
+
# Thin dispatcher: delegates IP geolocation to the configured provider.
|
|
5
|
+
# Switch providers via ContentSignals.configure { |c| c.geoip_provider = :ipinfo }
|
|
4
6
|
class VisitorLocationService
|
|
5
7
|
def self.locate(ip_address)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
db_path = ContentSignals.configuration.maxmind_db_path
|
|
9
|
-
return nil unless db_path && File.exist?(db_path)
|
|
10
|
-
|
|
11
|
-
# Initialize database connection (cached in production)
|
|
12
|
-
db = @db ||= MaxMindDB.new(db_path.to_s)
|
|
13
|
-
result = db.lookup(ip_address)
|
|
14
|
-
|
|
15
|
-
return nil unless result&.found?
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
country_code: result.country.iso_code,
|
|
19
|
-
country_name: result.country.name,
|
|
20
|
-
city: result.city.name,
|
|
21
|
-
region: result.subdivisions.most_specific&.name,
|
|
22
|
-
latitude: result.location.latitude,
|
|
23
|
-
longitude: result.location.longitude
|
|
24
|
-
}
|
|
25
|
-
rescue MaxMindDB::Error => e
|
|
26
|
-
Rails.logger.error "MaxMind lookup error: #{e.message}"
|
|
27
|
-
nil
|
|
28
|
-
rescue => e
|
|
29
|
-
Rails.logger.error "Geolocation error: #{e.message}"
|
|
30
|
-
nil
|
|
8
|
+
ContentSignals.configuration.resolved_geoip_provider.locate(ip_address)
|
|
31
9
|
end
|
|
32
10
|
|
|
11
|
+
# Convenience: is this a local/private IP?
|
|
33
12
|
def self.local_ip?(ip)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
ip_str = ip.to_s
|
|
37
|
-
ip_str.start_with?('127.', '192.168.', '10.', '172.', 'fc00:', 'fe80:') ||
|
|
38
|
-
ip_str == '::1' ||
|
|
39
|
-
ip_str == 'localhost'
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Fallback to API if database not available (optional)
|
|
43
|
-
def self.locate_via_api(ip_address)
|
|
44
|
-
return nil if ip_address.blank? || local_ip?(ip_address)
|
|
45
|
-
return nil unless defined?(HTTP)
|
|
46
|
-
|
|
47
|
-
token = ENV['IPINFO_TOKEN']
|
|
48
|
-
return nil unless token
|
|
49
|
-
|
|
50
|
-
# Using IPinfo.io as fallback
|
|
51
|
-
response = HTTP.get("https://ipinfo.io/#{ip_address}/json", params: { token: token })
|
|
52
|
-
data = JSON.parse(response.body)
|
|
53
|
-
|
|
54
|
-
return nil unless data['country']
|
|
55
|
-
|
|
56
|
-
lat, lng = data['loc']&.split(',')&.map(&:to_f)
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
country_code: data['country'],
|
|
60
|
-
country_name: country_name_from_code(data['country']),
|
|
61
|
-
city: data['city'],
|
|
62
|
-
region: data['region'],
|
|
63
|
-
latitude: lat,
|
|
64
|
-
longitude: lng
|
|
65
|
-
}
|
|
66
|
-
rescue => e
|
|
67
|
-
Rails.logger.error "API geolocation error: #{e.message}"
|
|
68
|
-
nil
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def self.country_name_from_code(code)
|
|
72
|
-
return nil unless defined?(ISO3166)
|
|
73
|
-
|
|
74
|
-
country = ISO3166::Country[code]
|
|
75
|
-
country&.name
|
|
76
|
-
rescue
|
|
77
|
-
nil
|
|
13
|
+
Geoip::BaseProvider.local_ip?(ip)
|
|
78
14
|
end
|
|
79
15
|
end
|
|
80
16
|
end
|
data/lib/content_signals.rb
CHANGED
|
@@ -5,17 +5,20 @@ require_relative "content_signals/configuration"
|
|
|
5
5
|
|
|
6
6
|
# Optional dependencies - gracefully handle if not installed
|
|
7
7
|
begin
|
|
8
|
-
require
|
|
8
|
+
require "browser"
|
|
9
9
|
rescue LoadError
|
|
10
10
|
# Browser gem not installed - device detection will be disabled
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
begin
|
|
14
|
-
require
|
|
14
|
+
require "maxminddb"
|
|
15
15
|
rescue LoadError
|
|
16
|
-
#
|
|
16
|
+
# maxminddb gem not installed - MaxmindProvider will be disabled
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
require "net/http"
|
|
20
|
+
require "json"
|
|
21
|
+
|
|
19
22
|
module ContentSignals
|
|
20
23
|
class Error < StandardError; end
|
|
21
24
|
|
|
@@ -44,6 +47,12 @@ if defined?(ActiveRecord)
|
|
|
44
47
|
require_relative "content_signals/models/page_view"
|
|
45
48
|
end
|
|
46
49
|
|
|
50
|
+
# Load geoip providers
|
|
51
|
+
require_relative "content_signals/geoip/base_provider"
|
|
52
|
+
require_relative "content_signals/geoip/maxmind_provider"
|
|
53
|
+
require_relative "content_signals/geoip/ipinfo_provider"
|
|
54
|
+
require_relative "content_signals/geoip/null_provider"
|
|
55
|
+
|
|
47
56
|
# Load services
|
|
48
57
|
require_relative "content_signals/services/page_view_tracker"
|
|
49
58
|
require_relative "content_signals/services/visitor_location_service"
|
|
@@ -52,6 +61,7 @@ require_relative "content_signals/services/device_detector_service"
|
|
|
52
61
|
# Load jobs if ActiveJob is available
|
|
53
62
|
if defined?(ActiveJob)
|
|
54
63
|
require_relative "content_signals/jobs/track_page_view_job"
|
|
64
|
+
require_relative "content_signals/jobs/update_geoip_database_job"
|
|
55
65
|
end
|
|
56
66
|
|
|
57
67
|
# Load concerns if ActiveSupport is available
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
5
|
|
|
6
6
|
module ContentSignals
|
|
7
7
|
module Generators
|
|
8
8
|
class InstallGenerator < Rails::Generators::Base
|
|
9
9
|
include Rails::Generators::Migration
|
|
10
10
|
|
|
11
|
-
source_root File.expand_path(
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
12
|
|
|
13
|
-
desc "Creates ContentSignals migration and
|
|
13
|
+
desc "Creates ContentSignals migration, initializer, and recurring job schedule"
|
|
14
14
|
|
|
15
15
|
def self.next_migration_number(path)
|
|
16
16
|
next_migration_number = current_migration_number(path) + 1
|
|
@@ -19,14 +19,42 @@ module ContentSignals
|
|
|
19
19
|
|
|
20
20
|
def copy_migration
|
|
21
21
|
migration_template "create_content_signals_page_views.rb.erb",
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
"db/migrate/create_content_signals_page_views.rb",
|
|
23
|
+
migration_version: migration_version
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def copy_initializer
|
|
27
27
|
template "content_signals.rb.erb", "config/initializers/content_signals.rb"
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def inject_recurring_job
|
|
31
|
+
recurring_yml = "config/recurring.yml"
|
|
32
|
+
|
|
33
|
+
if File.exist?(File.join(destination_root, recurring_yml))
|
|
34
|
+
content = File.read(File.join(destination_root, recurring_yml))
|
|
35
|
+
|
|
36
|
+
if content.include?("ContentSignals::UpdateGeoipDatabaseJob")
|
|
37
|
+
say_status :skip, "recurring.yml already contains ContentSignals::UpdateGeoipDatabaseJob", :yellow
|
|
38
|
+
else
|
|
39
|
+
append_to_file recurring_yml, <<~YAML
|
|
40
|
+
|
|
41
|
+
# ContentSignals β refresh GeoLite2-City database weekly (MaxMind updates Tuesdays)
|
|
42
|
+
content_signals_update_geoip_database:
|
|
43
|
+
class: ContentSignals::UpdateGeoipDatabaseJob
|
|
44
|
+
schedule: every week at 4am
|
|
45
|
+
YAML
|
|
46
|
+
say_status :append, recurring_yml, :green
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
create_file recurring_yml, <<~YAML
|
|
50
|
+
# ContentSignals β refresh GeoLite2-City database weekly (MaxMind updates Tuesdays)
|
|
51
|
+
content_signals_update_geoip_database:
|
|
52
|
+
class: ContentSignals::UpdateGeoipDatabaseJob
|
|
53
|
+
schedule: every week at 4am
|
|
54
|
+
YAML
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
30
58
|
def show_readme
|
|
31
59
|
readme "README" if behavior == :invoke
|
|
32
60
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "content_signals"
|
|
2
|
+
|
|
1
3
|
ContentSignals.configure do |config|
|
|
2
4
|
# Multi-tenancy (optional)
|
|
3
5
|
# Enable if your app has multiple tenants/accounts
|
|
@@ -9,9 +11,19 @@ ContentSignals.configure do |config|
|
|
|
9
11
|
config.redis_enabled = true
|
|
10
12
|
config.redis_namespace = "content_signals"
|
|
11
13
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
14
|
+
# IP Geolocation provider
|
|
15
|
+
# :maxmind β offline, fast, no API calls. Requires local .mmdb file (default)
|
|
16
|
+
# :ipinfo β online, no local file needed. Requires IPINFO_TOKEN env var
|
|
17
|
+
# :null β disables geolocation entirely
|
|
18
|
+
config.geoip_provider = :maxmind
|
|
19
|
+
|
|
20
|
+
# MaxMind GeoLite2 database path (used when geoip_provider = :maxmind)
|
|
21
|
+
# Initial download: bundle exec rake app:stejar:geoip:update (or run the job manually)
|
|
22
|
+
# Auto-updated weekly via ContentSignals::UpdateGeoipDatabaseJob (see config/recurring.yml)
|
|
23
|
+
config.maxmind_db_path = Rails.root.join("db", "GeoLite2-City.mmdb")
|
|
24
|
+
|
|
25
|
+
# IPinfo API token (used when geoip_provider = :ipinfo)
|
|
26
|
+
# config.geoip_token = ENV["IPINFO_TOKEN"]
|
|
15
27
|
|
|
16
28
|
# Tracking preferences
|
|
17
29
|
config.track_bots = false # Set to true to track bot/crawler visits
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- IonuΘ Munteanu Alexandru
|
|
@@ -116,7 +116,12 @@ files:
|
|
|
116
116
|
- lib/content_signals/concerns/trackable_page_views.rb
|
|
117
117
|
- lib/content_signals/configuration.rb
|
|
118
118
|
- lib/content_signals/engine.rb
|
|
119
|
+
- lib/content_signals/geoip/base_provider.rb
|
|
120
|
+
- lib/content_signals/geoip/ipinfo_provider.rb
|
|
121
|
+
- lib/content_signals/geoip/maxmind_provider.rb
|
|
122
|
+
- lib/content_signals/geoip/null_provider.rb
|
|
119
123
|
- lib/content_signals/jobs/track_page_view_job.rb
|
|
124
|
+
- lib/content_signals/jobs/update_geoip_database_job.rb
|
|
120
125
|
- lib/content_signals/models/page_view.rb
|
|
121
126
|
- lib/content_signals/services/device_detector_service.rb
|
|
122
127
|
- lib/content_signals/services/page_view_tracker.rb
|