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/README.md
CHANGED
|
@@ -1,28 +1,420 @@
|
|
|
1
1
|
# ContentSignals
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Listen to signals from your content.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A Rails engine for tracking page views and content engagement with rich analytics. Track views on any model (Pages, Posts, Events, Profiles) with demographics, device detection, and multi-tenant support.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 📊 **Page view tracking** with visitor identification
|
|
10
|
+
- 🌍 **Geolocation** (country, city, region) via MaxMind GeoLite2
|
|
11
|
+
- 📱 **Device detection** (mobile, tablet, desktop, hybrid apps)
|
|
12
|
+
- 🏢 **Multi-tenant support** (optional)
|
|
13
|
+
- 🤖 **Bot filtering** (automatically excludes crawlers)
|
|
14
|
+
- 🔄 **Background processing** (non-blocking with ActiveJob)
|
|
15
|
+
- 📈 **Rich analytics** with time-based scopes and aggregations
|
|
16
|
+
- 🎯 **Polymorphic tracking** (works with any model)
|
|
17
|
+
- 📲 **Hybrid app support** (Capacitor, Cordova, React Native, Flutter)
|
|
6
18
|
|
|
7
19
|
## Installation
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
Add this line to your application's Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'content_signals'
|
|
25
|
+
```
|
|
10
26
|
|
|
11
|
-
|
|
27
|
+
Then execute:
|
|
12
28
|
|
|
13
29
|
```bash
|
|
14
|
-
bundle
|
|
30
|
+
bundle install
|
|
15
31
|
```
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
Run the install generator:
|
|
18
34
|
|
|
19
35
|
```bash
|
|
20
|
-
|
|
36
|
+
rails generate content_signals:install
|
|
37
|
+
rails db:migrate
|
|
21
38
|
```
|
|
22
39
|
|
|
23
40
|
## Usage
|
|
24
41
|
|
|
25
|
-
|
|
42
|
+
### Basic Setup
|
|
43
|
+
|
|
44
|
+
#### 1. Add counter cache column to your trackable models
|
|
45
|
+
|
|
46
|
+
Add a `page_views_count` column to any model you want to track views for:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Migration example for Pages
|
|
50
|
+
class AddPageViewsCountToPages < ActiveRecord::Migration[7.0]
|
|
51
|
+
def change
|
|
52
|
+
add_column :pages, :page_views_count, :integer, default: 0, null: false
|
|
53
|
+
add_index :pages, :page_views_count
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Important:** The counter column must be named `page_views_count` (following Rails counter_cache conventions). The ContentSignals PageView model uses `counter_cache: :page_views_count` to automatically increment this column when view records are created.
|
|
59
|
+
|
|
60
|
+
For multiple trackable models:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# Add to any models you want to track
|
|
64
|
+
add_column :posts, :page_views_count, :integer, default: 0, null: false
|
|
65
|
+
add_column :events, :page_views_count, :integer, default: 0, null: false
|
|
66
|
+
add_column :profiles, :page_views_count, :integer, default: 0, null: false
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### 2. Include tracking in your controller
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
class PagesController < ApplicationController
|
|
73
|
+
include ContentSignals::TrackablePageViews
|
|
74
|
+
|
|
75
|
+
def show
|
|
76
|
+
@page = Page.find(params[:id])
|
|
77
|
+
# Page views are automatically tracked!
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**How it works:**
|
|
83
|
+
1. When a user visits a tracked page, the concern triggers tracking
|
|
84
|
+
2. A background job is enqueued to create a detailed PageView record
|
|
85
|
+
3. The `page_views_count` counter is automatically incremented via Rails counter_cache if the 'hit' is unique for the day
|
|
86
|
+
4. No blocking - the user gets instant response while tracking happens asynchronously
|
|
87
|
+
|
|
88
|
+
That's it! Page views will now be tracked automatically with:
|
|
89
|
+
- Total view counter via counter_cache (incremented when PageView record is created)
|
|
90
|
+
- Detailed PageView records with demographics (via background job)
|
|
91
|
+
- Visitor identification (authenticated users, device IDs, or anonymous)
|
|
92
|
+
- Device and location data
|
|
93
|
+
- Bot filtering
|
|
94
|
+
|
|
95
|
+
### Configuration
|
|
96
|
+
|
|
97
|
+
Create an initializer `config/initializers/content_signals.rb`:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
ContentSignals.configure do |config|
|
|
101
|
+
# Multi-tenancy (optional)
|
|
102
|
+
config.multitenancy = false
|
|
103
|
+
config.current_tenant_method = :current_tenant_id
|
|
104
|
+
config.tenant_model = 'Account'
|
|
105
|
+
|
|
106
|
+
# Redis for unique visitor tracking (optional)
|
|
107
|
+
# When enabled, prevents duplicate tracking of the same visitor on the same day
|
|
108
|
+
# Keys expire automatically after 24 hours
|
|
109
|
+
# Without Redis: all PageView records are still stored in the database,
|
|
110
|
+
# but the same visitor may create multiple records per day
|
|
111
|
+
config.redis_enabled = true
|
|
112
|
+
config.redis_namespace = 'content_signals'
|
|
113
|
+
|
|
114
|
+
# MaxMind GeoLite2 database path
|
|
115
|
+
config.maxmind_db_path = Rails.root.join('db', 'GeoLite2-City.mmdb')
|
|
116
|
+
|
|
117
|
+
# Tracking preferences
|
|
118
|
+
config.track_bots = false
|
|
119
|
+
config.track_admins = false
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Analytics Queries
|
|
124
|
+
|
|
125
|
+
Access rich analytics data through PageView scopes:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# Get page views for a specific page
|
|
129
|
+
page = Page.find(1)
|
|
130
|
+
page_views = ContentSignals::PageView.where(trackable: page)
|
|
131
|
+
|
|
132
|
+
# Time-based queries
|
|
133
|
+
page_views.today # returns all toyda's records
|
|
134
|
+
page_views.today.count # counter for today's views
|
|
135
|
+
page_views.yesterday
|
|
136
|
+
page_views.this_week
|
|
137
|
+
page_views.last_30_days
|
|
138
|
+
page_views.this_month
|
|
139
|
+
page_views.this_year
|
|
140
|
+
|
|
141
|
+
# Device filtering
|
|
142
|
+
page_views.mobile
|
|
143
|
+
page_views.desktop
|
|
144
|
+
page_views.tablet
|
|
145
|
+
|
|
146
|
+
# Location filtering
|
|
147
|
+
page_views.from_country('US')
|
|
148
|
+
page_views.from_city('New York')
|
|
149
|
+
|
|
150
|
+
# Platform filtering
|
|
151
|
+
page_views.from_website # Regular web browsers
|
|
152
|
+
page_views.from_hybrid_app # Mobile app WebViews
|
|
153
|
+
page_views.from_native_app # Native mobile apps
|
|
154
|
+
|
|
155
|
+
# User filtering
|
|
156
|
+
page_views.authenticated # Logged-in users
|
|
157
|
+
page_views.anonymous # Anonymous visitors
|
|
158
|
+
|
|
159
|
+
# Analytics aggregations
|
|
160
|
+
page_views.unique_count # Unique visitors
|
|
161
|
+
page_views.top_countries(10) # Top 10 countries
|
|
162
|
+
page_views.top_cities(10) # Top 10 cities
|
|
163
|
+
page_views.device_breakdown # Device type distribution
|
|
164
|
+
page_views.browser_breakdown # Browser distribution
|
|
165
|
+
page_views.location_heatmap # Lat/lng data for maps
|
|
166
|
+
|
|
167
|
+
# Growth rate
|
|
168
|
+
page_views.growth_rate(:this_week, :last_week) # Weekly growth %
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Example: Analytics Dashboard
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
class AnalyticsController < ApplicationController
|
|
175
|
+
def show
|
|
176
|
+
@page = Page.find(params[:id])
|
|
177
|
+
@views = ContentSignals::PageView.where(trackable: @page).last_30_days
|
|
178
|
+
|
|
179
|
+
@stats = {
|
|
180
|
+
total_views: @views.count,
|
|
181
|
+
unique_visitors: @views.unique_count,
|
|
182
|
+
top_countries: @views.top_countries(5),
|
|
183
|
+
top_cities: @views.top_cities(5),
|
|
184
|
+
device_breakdown: @views.device_breakdown,
|
|
185
|
+
growth_rate: @views.growth_rate(:this_week, :last_week)
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Custom Trackable Finder
|
|
192
|
+
|
|
193
|
+
By default, the concern looks for `@page`, `@post`, `@article`, `@profile`, `@event`, or `@trackable`. To customize:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
class ArticlesController < ApplicationController
|
|
197
|
+
include ContentSignals::TrackablePageViews
|
|
198
|
+
|
|
199
|
+
def show
|
|
200
|
+
@my_article = Article.find(params[:id])
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
def find_trackable_for_tracking
|
|
206
|
+
@my_article
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Skip Tracking Conditions
|
|
212
|
+
|
|
213
|
+
Override to add custom skip conditions:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
class PagesController < ApplicationController
|
|
217
|
+
include ContentSignals::TrackablePageViews
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
def should_skip_tracking?
|
|
222
|
+
super || draft_mode? || current_user&.internal?
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def draft_mode?
|
|
226
|
+
params[:draft].present?
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Manual Tracking
|
|
232
|
+
|
|
233
|
+
Track views programmatically without the controller concern:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
ContentSignals::PageViewTracker.track(
|
|
237
|
+
trackable: @page,
|
|
238
|
+
user: current_user,
|
|
239
|
+
request: request
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Multi-Tenant Setup
|
|
244
|
+
|
|
245
|
+
For multi-tenant applications:
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
# config/initializers/content_signals.rb
|
|
249
|
+
ContentSignals.configure do |config|
|
|
250
|
+
config.multitenancy = true
|
|
251
|
+
config.current_tenant_method = :current_account_id
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# In your ApplicationController
|
|
255
|
+
class ApplicationController < ActionController::Base
|
|
256
|
+
def current_account_id
|
|
257
|
+
Current.account_id # or however you track tenant context
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
All PageView queries will automatically scope to the current tenant.
|
|
263
|
+
|
|
264
|
+
### Hybrid Mobile App Support
|
|
265
|
+
|
|
266
|
+
Track views from mobile apps (Capacitor, Cordova, React Native, Flutter):
|
|
267
|
+
|
|
268
|
+
#### Send custom headers from your app:
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
// In your mobile app
|
|
272
|
+
fetch('https://yoursite.com/pages/1', {
|
|
273
|
+
headers: {
|
|
274
|
+
'X-App-Platform': 'capacitor',
|
|
275
|
+
'X-App-Version': '1.2.3',
|
|
276
|
+
'X-Device-ID': 'unique-device-uuid'
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### Or use URL parameters:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
https://yoursite.com/pages/1?app_platform=capacitor&device_id=abc123&app_version=1.2.3
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Views from mobile apps will be automatically detected and tracked with `app_platform` and `device_id`.
|
|
288
|
+
|
|
289
|
+
### Geolocation Setup
|
|
290
|
+
|
|
291
|
+
1. **Sign up for MaxMind GeoLite2** (free): https://www.maxmind.com/en/geolite2/signup
|
|
292
|
+
2. **Download GeoLite2-City.mmdb** database
|
|
293
|
+
3. **Place in:** `db/GeoLite2-City.mmdb`
|
|
294
|
+
4. **Configure path** in initializer (shown above)
|
|
295
|
+
|
|
296
|
+
The gem will automatically enrich page views with:
|
|
297
|
+
- Country (code and name)
|
|
298
|
+
- City
|
|
299
|
+
- Region/State
|
|
300
|
+
- Latitude/Longitude
|
|
301
|
+
|
|
302
|
+
### PageView Model
|
|
303
|
+
|
|
304
|
+
Access individual page view records:
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
view = ContentSignals::PageView.last
|
|
308
|
+
|
|
309
|
+
view.trackable # => #<Page id: 1>
|
|
310
|
+
view.user # => #<User id: 5> (if authenticated)
|
|
311
|
+
view.visitor_id # => "user_5" or "device_abc123" or "anon_1a2b3c"
|
|
312
|
+
view.country_name # => "United States"
|
|
313
|
+
view.city # => "New York"
|
|
314
|
+
view.device_type # => "mobile"
|
|
315
|
+
view.browser # => "Chrome"
|
|
316
|
+
view.os # => "iOS"
|
|
317
|
+
view.app_platform # => "capacitor" (if from mobile app)
|
|
318
|
+
view.viewed_at # => 2026-01-06 10:30:00 UTC
|
|
319
|
+
|
|
320
|
+
# Helper methods
|
|
321
|
+
view.authenticated? # => true if user present
|
|
322
|
+
view.anonymous? # => true if no user
|
|
323
|
+
view.mobile_device? # => true if mobile
|
|
324
|
+
view.desktop_device? # => true if desktop
|
|
325
|
+
view.hybrid_app? # => true if from mobile app
|
|
326
|
+
view.web_browser? # => true if from web browser
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Advanced Usage
|
|
330
|
+
|
|
331
|
+
### Custom User Method
|
|
332
|
+
|
|
333
|
+
If you use a different method name for current user:
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
class PagesController < ApplicationController
|
|
337
|
+
include ContentSignals::TrackablePageViews
|
|
338
|
+
|
|
339
|
+
private
|
|
340
|
+
|
|
341
|
+
def current_user_for_tracking
|
|
342
|
+
current_person # or whatever your method is called
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Background Job Queue
|
|
348
|
+
|
|
349
|
+
By default, tracking jobs use the `default` queue. To customize:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
# config/initializers/content_signals.rb
|
|
353
|
+
ContentSignals::TrackPageViewJob.queue_as :analytics
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Export Analytics Data
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
# Export to CSV
|
|
360
|
+
require 'csv'
|
|
361
|
+
|
|
362
|
+
CSV.generate(headers: true) do |csv|
|
|
363
|
+
csv << ['Date', 'Country', 'City', 'Device', 'Browser']
|
|
364
|
+
|
|
365
|
+
ContentSignals::PageView.where(trackable: @page).find_each do |view|
|
|
366
|
+
csv << [
|
|
367
|
+
view.viewed_at.to_date,
|
|
368
|
+
view.country_name,
|
|
369
|
+
view.city,
|
|
370
|
+
view.device_type,
|
|
371
|
+
view.browser
|
|
372
|
+
]
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Requirements
|
|
378
|
+
|
|
379
|
+
- Rails >= 7.0
|
|
380
|
+
- Ruby >= 3.0
|
|
381
|
+
- ActiveJob (for background processing)
|
|
382
|
+
- Redis (optional, for unique visitor tracking)
|
|
383
|
+
|
|
384
|
+
### Optional Dependencies
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
# Gemfile
|
|
388
|
+
gem 'maxminddb' # For IP geolocation
|
|
389
|
+
gem 'browser' # For device/browser detection
|
|
390
|
+
gem 'redis' # For unique visitor tracking
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Performance
|
|
394
|
+
|
|
395
|
+
- **Fast**: Counter increments are synchronous (< 1ms)
|
|
396
|
+
- **Non-blocking**: Detailed tracking happens in background jobs
|
|
397
|
+
- **Scalable**: Uses counter caching and database indexes
|
|
398
|
+
- **Efficient**: Redis-based unique visitor deduplication (optional)
|
|
399
|
+
|
|
400
|
+
## Testing
|
|
401
|
+
|
|
402
|
+
In your test environment, you may want to disable tracking:
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
# config/environments/test.rb
|
|
406
|
+
config.after_initialize do
|
|
407
|
+
ContentSignals.configure do |config|
|
|
408
|
+
config.track_bots = true # Allow test requests
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Or stub the tracker in specs:
|
|
414
|
+
|
|
415
|
+
```ruby
|
|
416
|
+
allow(ContentSignals::PageViewTracker).to receive(:track)
|
|
417
|
+
```
|
|
26
418
|
|
|
27
419
|
## Development
|
|
28
420
|
|