content_signals 0.1.0 → 0.1.1

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.
data/READY_TO_GEM.md ADDED
@@ -0,0 +1,375 @@
1
+ # ✅ Ready to Extract into Gem
2
+
3
+ ## What You Already Have (90% Complete)
4
+
5
+ ### ✅ Complete Specification
6
+ - [COUNTERS.MD](COUNTERS.MD) - 1,500+ lines of detailed implementation
7
+ - All services designed and documented
8
+ - Database schema with migrations
9
+ - API endpoints defined
10
+ - Analytics queries specified
11
+ - Testing requirements outlined
12
+
13
+ ### ✅ Core Features Designed
14
+ 1. **PageViewTracker Service** - Visitor identification, data compilation, Redis deduplication
15
+ 2. **TrackPageViewJob** - Background async processing with retry logic
16
+ 3. **VisitorLocationService** - MaxMind GeoLite2 integration
17
+ 4. **DeviceDetectorService** - Browser gem, hybrid app detection
18
+ 5. **AnalyticsQueryService** - Top countries, cities, devices, time series
19
+ 6. **PageView Model** - Polymorphic trackable, tenant scoping, analytics methods
20
+ 7. **Trackable Concern** - Include in any model (Page, Profile, Event)
21
+ 8. **Configuration DSL** - Multitenancy, Redis, MaxMind setup
22
+
23
+ ### ✅ Multi-Tenant Support
24
+ - Optional `tenant_id` on all tables
25
+ - Automatic tenant scoping with default_scope
26
+ - ActsAsTenant & Apartment gem compatibility
27
+ - Tenant-aware Redis keys
28
+ - Cross-tenant admin analytics
29
+
30
+ ### ✅ Hybrid Mobile App Support
31
+ - Capacitor, Cordova, React Native, Flutter detection
32
+ - Custom headers: X-App-Platform, X-App-Version, X-Device-ID
33
+ - URL parameter fallbacks
34
+ - WebView user agent detection
35
+ - Persistent device_id tracking
36
+
37
+ ### ✅ Architecture Decisions Made
38
+ - Redis for 24-hour unique visitor deduplication (optional)
39
+ - PostgreSQL for persistent PageView records
40
+ - Background jobs for async processing
41
+ - Counter caching for performance
42
+ - Bot/crawler exclusion
43
+ - Admin user exclusion (optional)
44
+
45
+ ### ✅ Performance Validated
46
+ - Redis: 1,000 requests/second (only 0.2-1% capacity)
47
+ - Background jobs: Non-blocking
48
+ - Counter cache: Instant `page_views_count` access
49
+ - Indexed queries: Fast analytics
50
+ - Optional Redis: Works PostgreSQL-only
51
+
52
+ ---
53
+
54
+ ## What Needs to Be Done (10% Remaining)
55
+
56
+ ### 🔨 Implementation Tasks (10-12 hours)
57
+
58
+ #### 1. Gem Scaffolding (30 minutes)
59
+ ```bash
60
+ bundle gem modest_analytics --test=rspec --ci=github --mit
61
+ ```
62
+ - Update gemspec (dependencies, description)
63
+ - Create lib/modest_analytics.rb entry point
64
+ - Create lib/modest_analytics/engine.rb Rails engine
65
+ - Set version to 0.1.0
66
+
67
+ #### 2. Copy Code from COUNTERS.MD (4 hours)
68
+ Simply copy these 8 components from specification to gem:
69
+
70
+ | Component | From COUNTERS.MD | To Gem |
71
+ |-----------|------------------|---------|
72
+ | PageView model | Lines 990-1050 | lib/modest_analytics/models/page_view.rb |
73
+ | PageViewTracker | Lines 860-940 | lib/modest_analytics/services/page_view_tracker.rb |
74
+ | TrackPageViewJob | Lines 950-980 | lib/modest_analytics/jobs/track_page_view_job.rb |
75
+ | VisitorLocationService | Lines 420-480 | lib/modest_analytics/services/visitor_location_service.rb |
76
+ | DeviceDetectorService | Lines 570-620 | lib/modest_analytics/services/device_detector_service.rb |
77
+ | AnalyticsQueryService | Lines 1090-1180 | lib/modest_analytics/services/analytics_query_service.rb |
78
+ | Trackable concern | Lines 1040-1070 | lib/modest_analytics/concerns/trackable.rb |
79
+ | Configuration | Lines 172-189 | lib/modest_analytics/configuration.rb |
80
+
81
+ **No new code needed** - just copy & paste with minor namespace adjustments!
82
+
83
+ #### 3. Migrations (30 minutes)
84
+ Copy migration from COUNTERS.MD lines 700-750:
85
+ - `db/migrate/001_create_modest_analytics_page_views.rb`
86
+
87
+ #### 4. Generator (1 hour)
88
+ Create `lib/generators/modest_analytics/install_generator.rb`:
89
+ - Copy migrations to app
90
+ - Create initializer
91
+ - Display setup instructions
92
+
93
+ #### 5. Tests (3 hours)
94
+ Write RSpec tests for:
95
+ - PageView model (scopes, methods)
96
+ - All 4 services (mocked)
97
+ - Background job
98
+ - Integration test (end-to-end)
99
+
100
+ #### 6. Documentation (2 hours)
101
+ - README.md (installation, quick start, examples)
102
+ - CHANGELOG.md (v0.1.0 release notes)
103
+ - docs/CONFIGURATION.md
104
+ - docs/MULTITENANCY.md
105
+ - docs/HYBRID_APPS.md
106
+
107
+ #### 7. Polish & Release (1 hour)
108
+ - RuboCop code style
109
+ - Test in Stejar locally
110
+ - Build & publish to RubyGems
111
+ - Push to GitHub
112
+ - Create v0.1.0 release
113
+
114
+ ---
115
+
116
+ ## File-by-File Mapping
117
+
118
+ ### From COUNTERS.MD → Gem Structure
119
+
120
+ ```
121
+ COUNTERS.MD (1,500 lines)
122
+ ├── Lines 1-80: Overview & Requirements
123
+ │ → README.md introduction
124
+
125
+ ├── Lines 172-278: Multi-Tenant Support
126
+ │ → lib/modest_analytics/configuration.rb
127
+ │ → docs/MULTITENANCY.md
128
+
129
+ ├── Lines 280-380: Hybrid Mobile App Support
130
+ │ → docs/HYBRID_APPS.md
131
+
132
+ ├── Lines 382-540: Geolocation Services
133
+ │ → lib/modest_analytics/services/visitor_location_service.rb
134
+
135
+ ├── Lines 570-620: Device Detection
136
+ │ → lib/modest_analytics/services/device_detector_service.rb
137
+
138
+ ├── Lines 700-750: Database Migrations
139
+ │ → db/migrate/001_create_modest_analytics_page_views.rb
140
+
141
+ ├── Lines 860-940: PageViewTracker
142
+ │ → lib/modest_analytics/services/page_view_tracker.rb
143
+
144
+ ├── Lines 950-980: TrackPageViewJob
145
+ │ → lib/modest_analytics/jobs/track_page_view_job.rb
146
+
147
+ ├── Lines 990-1070: PageView Model + Trackable Concern
148
+ │ → lib/modest_analytics/models/page_view.rb
149
+ │ → lib/modest_analytics/concerns/trackable.rb
150
+
151
+ ├── Lines 1090-1180: AnalyticsQueryService
152
+ │ → lib/modest_analytics/services/analytics_query_service.rb
153
+
154
+ └── Lines 1400-1461: Analytics Queries Examples
155
+ → README.md examples section
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Dependencies Already Defined
161
+
162
+ ### Required
163
+ ```ruby
164
+ # In gemspec
165
+ spec.add_dependency "rails", ">= 7.0"
166
+ spec.add_dependency "maxmind-geoip2" # IP geolocation
167
+ spec.add_dependency "browser" # Device detection
168
+ ```
169
+
170
+ ### Optional (Peer Dependencies)
171
+ ```ruby
172
+ # User's Gemfile
173
+ gem 'redis' # For unique visitor deduplication
174
+ gem 'acts_as_tenant' # For multitenancy (optional)
175
+ gem 'apartment' # Alternative multitenancy (optional)
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Quick Start Guide (After Publishing)
181
+
182
+ ### 1. Install in Stejar CMS
183
+
184
+ ```ruby
185
+ # Gemfile
186
+ gem 'modest_analytics'
187
+
188
+ # Terminal
189
+ bundle install
190
+ rails generate modest_analytics:install
191
+ rails db:migrate
192
+
193
+ # app/models/stejar/page.rb
194
+ module Stejar
195
+ class Page < ApplicationRecord
196
+ include ModestAnalytics::Trackable
197
+ end
198
+ end
199
+
200
+ # app/controllers/stejar/pages_controller.rb
201
+ def show
202
+ @page = Stejar::Page.find(params[:id])
203
+ @page.track_view(user: current_user, request: request)
204
+ end
205
+
206
+ # app/views/stejar/pages/show.html.erb
207
+ <p><%= @page.page_views_count %> views</p>
208
+ <p><%= @page.analytics.unique_views_today %> unique today</p>
209
+ ```
210
+
211
+ ### 2. Install in Eventya
212
+
213
+ ```ruby
214
+ # app/models/profile.rb
215
+ class Profile < ApplicationRecord
216
+ include ModestAnalytics::Trackable
217
+ end
218
+
219
+ # app/models/event.rb
220
+ class Event < ApplicationRecord
221
+ include ModestAnalytics::Trackable
222
+ end
223
+
224
+ # Controllers
225
+ @profile.track_view(user: current_user, request: request)
226
+ @event.track_view(user: current_user, request: request)
227
+
228
+ # Views
229
+ <%= @profile.page_views_count %> profile views
230
+ <%= @event.analytics.top_countries.first %> most popular country
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Testing Strategy
236
+
237
+ ### Unit Tests (50% of testing time)
238
+ ```ruby
239
+ # spec/models/page_view_spec.rb
240
+ describe ModestAnalytics::PageView do
241
+ it { should belong_to(:trackable) }
242
+ it { should belong_to(:tenant).optional }
243
+ it { should belong_to(:user).optional }
244
+
245
+ describe '.today' do
246
+ # Test scope
247
+ end
248
+
249
+ describe '.unique_count' do
250
+ # Test analytics method
251
+ end
252
+ end
253
+
254
+ # spec/services/page_view_tracker_spec.rb
255
+ describe ModestAnalytics::PageViewTracker do
256
+ describe '#track' do
257
+ it 'increments page_views_count'
258
+ it 'enqueues background job'
259
+ it 'tracks unique view if first today'
260
+ it 'skips duplicate view same day'
261
+ end
262
+
263
+ describe '#identify_visitor' do
264
+ it 'prefers user_id'
265
+ it 'falls back to device_id'
266
+ it 'falls back to cookie'
267
+ it 'falls back to IP hash'
268
+ end
269
+ end
270
+ ```
271
+
272
+ ### Integration Tests (30% of testing time)
273
+ ```ruby
274
+ # spec/integration/tracking_spec.rb
275
+ describe 'End-to-end tracking' do
276
+ it 'tracks page view from web browser' do
277
+ page = create(:page)
278
+ user = create(:user)
279
+
280
+ # Simulate controller action
281
+ request = mock_request(ip: '1.2.3.4', user_agent: 'Mozilla...')
282
+ page.track_view(user: user, request: request)
283
+
284
+ expect(page.page_views_count).to eq(1)
285
+ expect(TrackPageViewJob).to have_been_enqueued
286
+
287
+ # Process job
288
+ perform_enqueued_jobs
289
+
290
+ page_view = ModestAnalytics::PageView.last
291
+ expect(page_view.trackable).to eq(page)
292
+ expect(page_view.user).to eq(user)
293
+ expect(page_view.country_code).to be_present
294
+ end
295
+
296
+ it 'tracks from Capacitor mobile app' do
297
+ # Test hybrid app detection
298
+ end
299
+
300
+ it 'respects tenant isolation' do
301
+ # Test multitenancy
302
+ end
303
+ end
304
+ ```
305
+
306
+ ### Manual Testing (20% of testing time)
307
+ 1. Install in dummy Rails app
308
+ 2. Track views on test pages
309
+ 3. Check analytics queries
310
+ 4. Test MaxMind download
311
+ 5. Test Redis caching
312
+ 6. Test hybrid app headers
313
+
314
+ ---
315
+
316
+ ## Success Metrics
317
+
318
+ ### Technical Metrics
319
+ - [ ] All tests passing (RSpec green)
320
+ - [ ] Code coverage >80%
321
+ - [ ] RuboCop violations: 0
322
+ - [ ] Gem builds successfully
323
+ - [ ] Installs in Stejar without errors
324
+ - [ ] Installs in Eventya without errors
325
+
326
+ ### Release Metrics
327
+ - [ ] Published to RubyGems.org
328
+ - [ ] GitHub repo with README
329
+ - [ ] v0.1.0 release created
330
+ - [ ] License file (MIT) included
331
+ - [ ] Changelog with release notes
332
+
333
+ ### Usage Metrics (First Week)
334
+ - [ ] Stejar using gem in production
335
+ - [ ] Eventya using gem in production
336
+ - [ ] 0-1 GitHub issues opened (low bug count)
337
+ - [ ] 5+ stars on GitHub (if announced)
338
+
339
+ ---
340
+
341
+ ## Risk Assessment
342
+
343
+ ### Low Risk ✅
344
+ - **Code quality** - Already designed, just needs copying
345
+ - **Dependencies** - Minimal, well-maintained gems
346
+ - **Scope** - Small, focused gem (single responsibility)
347
+ - **Use cases** - Clear, well-defined (Stejar, Eventya)
348
+
349
+ ### Medium Risk ⚠️
350
+ - **Maintenance** - Will need occasional updates (1-2 hours/month)
351
+ - **Support** - GitHub issues from other users
352
+ - **Documentation** - Must be clear for external users
353
+ - **Breaking changes** - Need semantic versioning
354
+
355
+ ### Mitigation Strategies
356
+ 1. **Set expectations** - README "nights-and-weekends project"
357
+ 2. **Issue templates** - Guide users to self-service
358
+ 3. **Close stale issues** - 30 days without response
359
+ 4. **Encourage PRs** - "Code contributions welcome"
360
+ 5. **Version carefully** - Use semver, document breaking changes
361
+
362
+ ---
363
+
364
+ ## Decision Matrix
365
+
366
+ | Factor | Build Now | Extract Later |
367
+ |--------|-----------|---------------|
368
+ | **Time to production** | 2 days + gem | Immediate (in Stejar) |
369
+ | **Code reuse** | ✅ Single gem | ❌ Duplicate in Eventya |
370
+ | **Maintenance** | ⚠️ GitHub issues | ✅ Internal only |
371
+ | **Learning** | ✅ Gem authoring | ❌ No new skills |
372
+ | **Community impact** | ✅ Help others | ❌ Private code |
373
+ | **Resume value** | ✅ Open source | ❌ Not visible |
374
+ | **Flexibility** | ⚠️ Breaking changes hard | ✅ Easy iteration |
375
+ | **Testing** | ✅ Community feedback | ⚠️ Only your use cases |
data/STRUCTURE.md ADDED
@@ -0,0 +1,108 @@
1
+ # Content Signals Gem - Project Structure
2
+
3
+ ## ✅ Completed Setup
4
+
5
+ ### Directory Structure
6
+ ```
7
+ lib/
8
+ ├── content_signals.rb # Main entry point
9
+ ├── content_signals/
10
+ │ ├── version.rb # Version constant
11
+ │ ├── configuration.rb # Configuration DSL
12
+ │ ├── engine.rb # Rails engine integration
13
+ │ ├── models/ # ActiveRecord models
14
+ │ ├── services/ # Business logic services
15
+ │ ├── concerns/ # ActiveSupport concerns
16
+ │ └── jobs/ # ActiveJob background jobs
17
+ └── generators/
18
+ └── content_signals/ # Rails generators
19
+
20
+ spec/
21
+ ├── spec_helper.rb # Basic RSpec config
22
+ ├── rails_helper.rb # Rails + DB config
23
+ ├── support/
24
+ │ ├── schema.rb # Test database schema
25
+ │ ├── models.rb # Test models (Page, Profile, User)
26
+ │ └── request_helpers.rb # Mock request helpers
27
+ ├── models/ # Model specs
28
+ ├── services/ # Service specs
29
+ └── jobs/ # Job specs
30
+ ```
31
+
32
+ ### Dependencies Installed
33
+ - **Rails** >= 7.0
34
+ - **maxmind-geoip2** ~> 1.1 (IP geolocation)
35
+ - **browser** ~> 5.0 (device detection)
36
+ - **sqlite3** >= 2.1 (testing)
37
+ - **rspec** ~> 3.0 (testing framework)
38
+
39
+ ### Configuration System
40
+ ```ruby
41
+ ContentSignals.configure do |config|
42
+ config.multitenancy = true
43
+ config.current_tenant_method = :current_tenant_id
44
+ config.tenant_model = 'Account'
45
+ config.redis_enabled = true
46
+ config.redis_namespace = 'content_signals'
47
+ config.maxmind_db_path = Rails.root.join('db', 'GeoLite2-City.mmdb')
48
+ config.track_bots = false
49
+ config.track_admins = false
50
+ end
51
+ ```
52
+
53
+ ### Test Database Schema
54
+ - ✅ `pages` table (test trackable model)
55
+ - ✅ `profiles` table (test trackable model)
56
+ - ✅ `users` table
57
+ - ✅ `content_signals_page_views` table (full schema from specs)
58
+
59
+ ### Testing Setup
60
+ - ✅ RSpec configured
61
+ - ✅ In-memory SQLite database
62
+ - ✅ Transaction rollback between tests
63
+ - ✅ Mock request helpers
64
+ - ✅ Test models defined
65
+ - ✅ All tests passing (7 examples, 0 failures)
66
+
67
+ ## Next Steps
68
+
69
+ ### 1. Core Models
70
+ - [ ] `ContentSignals::PageView` model with scopes
71
+ - [ ] Test specs for PageView model
72
+
73
+ ### 2. Services
74
+ - [ ] `PageViewTracker` - Main tracking service
75
+ - [ ] `VisitorLocationService` - MaxMind geolocation
76
+ - [ ] `DeviceDetectorService` - Browser detection
77
+ - [ ] `AnalyticsQueryService` - Query interface
78
+ - [ ] Test specs for all services
79
+
80
+ ### 3. Background Jobs
81
+ - [ ] `TrackPageViewJob` - Async processing
82
+ - [ ] Test specs for job
83
+
84
+ ### 4. Concerns
85
+ - [ ] `Trackable` concern for models
86
+ - [ ] Test specs for concern
87
+
88
+ ### 5. Generators
89
+ - [ ] Install generator (migrations, initializer)
90
+ - [ ] Test generator output
91
+
92
+ ### 6. Documentation
93
+ - [ ] Update README with usage examples
94
+ - [ ] Add CHANGELOG entry
95
+ - [ ] Create migration templates
96
+
97
+ ## Running Tests
98
+ ```bash
99
+ bundle exec rspec # Run all tests
100
+ bundle exec rspec spec/models/ # Run model tests
101
+ bundle exec rspec spec/services/ # Run service tests
102
+ ```
103
+
104
+ ## Current Status
105
+ ✅ Gem structure complete
106
+ ✅ Testing environment working
107
+ ✅ Configuration system tested
108
+ ⏳ Ready to implement core functionality
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContentSignals
4
+ module TrackablePageViews
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ after_action :track_page_view, only: [:show]
9
+ end
10
+
11
+ private
12
+
13
+ def track_page_view
14
+ return if should_skip_tracking?
15
+
16
+ trackable = find_trackable_for_tracking
17
+ return unless trackable
18
+
19
+ # Ensure visitor cookie is set
20
+ ensure_visitor_cookie
21
+
22
+ PageViewTracker.track(
23
+ trackable: trackable,
24
+ user: current_user_for_tracking,
25
+ request: request
26
+ )
27
+ rescue => e
28
+ Rails.logger.error "Failed to track page view: #{e.message}"
29
+ # Don't raise - tracking failures shouldn't break the request
30
+ end
31
+
32
+ def ensure_visitor_cookie
33
+ return if cookies.signed[:visitor_id].present?
34
+ return if current_user_for_tracking.present? # Don't need cookie for authenticated users
35
+
36
+ cookies.signed[:visitor_id] = {
37
+ value: "visitor_#{SecureRandom.uuid}",
38
+ expires: 2.years.from_now,
39
+ httponly: true,
40
+ same_site: :lax
41
+ }
42
+ rescue => e
43
+ Rails.logger.error "Failed to set visitor cookie: #{e.message}"
44
+ end
45
+
46
+ def should_skip_tracking?
47
+ bot_request? ||
48
+ admin_viewing? ||
49
+ preview_mode?
50
+ end
51
+
52
+ def find_trackable_for_tracking
53
+ # Look for common instance variable names
54
+ # Override this method in your controller if you use different naming
55
+ @page || @post || @article || @profile || @event || @trackable
56
+ end
57
+
58
+ def current_user_for_tracking
59
+ # Override this method if you use a different method name
60
+ current_user if respond_to?(:current_user, true)
61
+ end
62
+
63
+ def bot_request?
64
+ return false unless defined?(Browser)
65
+
66
+ browser = Browser.new(request.user_agent)
67
+ browser.bot? || browser.search_engine?
68
+ rescue
69
+ false
70
+ end
71
+
72
+ def admin_viewing?
73
+ user = current_user_for_tracking
74
+ return false unless user
75
+
76
+ # Check common admin patterns
77
+ user.respond_to?(:admin?) && user.admin?
78
+ rescue
79
+ false
80
+ end
81
+
82
+ def preview_mode?
83
+ params[:preview].present? || session[:preview_mode]
84
+ rescue
85
+ false
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContentSignals
4
+ class Configuration
5
+ attr_accessor :multitenancy,
6
+ :current_tenant_method,
7
+ :tenant_model,
8
+ :redis_enabled,
9
+ :redis_namespace,
10
+ :maxmind_db_path,
11
+ :track_bots,
12
+ :track_admins
13
+
14
+ def initialize
15
+ @multitenancy = false
16
+ @current_tenant_method = :current_tenant_id
17
+ @tenant_model = nil
18
+ @redis_enabled = true
19
+ @redis_namespace = "content_signals"
20
+ @maxmind_db_path = default_maxmind_path
21
+ @track_bots = false
22
+ @track_admins = false
23
+ end
24
+
25
+ def multitenancy?
26
+ @multitenancy
27
+ end
28
+
29
+ def redis_enabled?
30
+ @redis_enabled && defined?(Redis)
31
+ end
32
+
33
+ private
34
+
35
+ def default_maxmind_path
36
+ return nil unless defined?(Rails) && Rails.respond_to?(:root) && Rails.root
37
+
38
+ Rails.root.join("db", "GeoLite2-City.mmdb")
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "active_record"
5
+
6
+ module ContentSignals
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace ContentSignals
9
+
10
+ config.generators do |g|
11
+ g.test_framework :rspec
12
+ g.fixture_replacement :factory_bot
13
+ g.factory_bot dir: "spec/factories"
14
+ end
15
+
16
+ # Eager load models, services, jobs, concerns
17
+ config.eager_load_paths += %W[
18
+ #{root}/lib/content_signals/models
19
+ #{root}/lib/content_signals/services
20
+ #{root}/lib/content_signals/jobs
21
+ #{root}/lib/content_signals/concerns
22
+ ]
23
+
24
+ initializer "content_signals.active_job" do
25
+ config.after_initialize do
26
+ if defined?(ActiveJob)
27
+ require_relative "jobs/track_page_view_job" if File.exist?(File.join(__dir__, "jobs", "track_page_view_job.rb"))
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContentSignals
4
+ class TrackPageViewJob < ActiveJob::Base
5
+ queue_as :default
6
+
7
+ # Retry on database errors but not on validation errors
8
+ retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
9
+ discard_on ActiveRecord::RecordInvalid
10
+
11
+ def perform(trackable_type, trackable_id, user_id, tracking_data)
12
+ PageView.create!(
13
+ tenant_id: tracking_data['tenant_id'] || tracking_data[:tenant_id],
14
+ trackable_type: trackable_type,
15
+ trackable_id: trackable_id,
16
+ user_id: user_id,
17
+ visitor_id: tracking_data['visitor_id'] || tracking_data[:visitor_id],
18
+ ip_address: tracking_data['ip_address'] || tracking_data[:ip_address],
19
+ user_agent: tracking_data['user_agent'] || tracking_data[:user_agent],
20
+ referrer: tracking_data['referrer'] || tracking_data[:referrer],
21
+ country_code: tracking_data['country_code'] || tracking_data[:country_code],
22
+ country_name: tracking_data['country_name'] || tracking_data[:country_name],
23
+ city: tracking_data['city'] || tracking_data[:city],
24
+ region: tracking_data['region'] || tracking_data[:region],
25
+ latitude: tracking_data['latitude'] || tracking_data[:latitude],
26
+ longitude: tracking_data['longitude'] || tracking_data[:longitude],
27
+ locale: tracking_data['locale'] || tracking_data[:locale],
28
+ device_type: tracking_data['device_type'] || tracking_data[:device_type],
29
+ browser: tracking_data['browser'] || tracking_data[:browser],
30
+ os: tracking_data['os'] || tracking_data[:os],
31
+ app_platform: tracking_data['app_platform'] || tracking_data[:app_platform],
32
+ app_version: tracking_data['app_version'] || tracking_data[:app_version],
33
+ device_id: tracking_data['device_id'] || tracking_data[:device_id],
34
+ viewed_at: tracking_data['viewed_at'] || tracking_data[:viewed_at] || Time.current
35
+ )
36
+ rescue => e
37
+ Rails.logger.error "Failed to track page view: #{e.message}"
38
+ Rails.logger.error e.backtrace.join("\n")
39
+ # Optionally send to error tracking service (Sentry, Rollbar, etc.)
40
+ raise unless Rails.env.production? # Re-raise in non-production for debugging
41
+ end
42
+ end
43
+ end