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.
- checksums.yaml +4 -4
- data/.rubocop.yml +48 -0
- data/.rubocop_todo.yml +146 -0
- data/COUNTERS.MD +1522 -0
- data/README.md +400 -8
- data/READY_TO_GEM.md +375 -0
- data/STRUCTURE.md +108 -0
- data/lib/content_signals/concerns/trackable_page_views.rb +88 -0
- data/lib/content_signals/configuration.rb +41 -0
- data/lib/content_signals/engine.rb +33 -0
- data/lib/content_signals/jobs/track_page_view_job.rb +43 -0
- data/lib/content_signals/models/page_view.rb +229 -0
- data/lib/content_signals/services/device_detector_service.rb +42 -0
- data/lib/content_signals/services/page_view_tracker.rb +147 -0
- data/lib/content_signals/services/visitor_location_service.rb +80 -0
- data/lib/content_signals/version.rb +1 -1
- data/lib/content_signals.rb +53 -1
- data/lib/generators/content_signals/install_generator.rb +41 -0
- data/lib/generators/content_signals/templates/README +36 -0
- data/lib/generators/content_signals/templates/content_signals.rb.erb +19 -0
- data/lib/generators/content_signals/templates/create_content_signals_page_views.rb.erb +48 -0
- metadata +31 -14
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
|