omnitrack-rb 2.0.0 → 3.0.0
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/.env.example +6 -0
- data/AI_GEM_SETUP.md +8 -0
- data/README.md +41 -0
- data/app/controllers/omnitrack/application_controller.rb +24 -0
- data/app/controllers/omnitrack/path/application_controller.rb +15 -0
- data/app/controllers/omnitrack/path/sessions_controller.rb +39 -0
- data/app/controllers/omnitrack/path/visit_events_controller.rb +21 -0
- data/app/controllers/omnitrack/sessions_controller.rb +37 -0
- data/app/controllers/omnitrack/visit_events_controller.rb +19 -0
- data/app/models/omnitrack/application_record.rb +7 -0
- data/app/models/omnitrack/delivery_status.rb +12 -0
- data/app/models/omnitrack/visit_event.rb +15 -0
- data/app/views/layouts/omnitrack/admin.html.erb +55 -0
- data/app/views/omnitrack/path/sessions/new.html.erb +16 -0
- data/app/views/omnitrack/path/visit_events/index.html.erb +50 -0
- data/app/views/omnitrack/path/visit_events/show.html.erb +51 -0
- data/app/views/omnitrack/sessions/new.html.erb +16 -0
- data/app/views/omnitrack/visit_events/index.html.erb +50 -0
- data/app/views/omnitrack/visit_events/show.html.erb +51 -0
- data/config/locales/ar.yml +33 -0
- data/config/locales/en.yml +33 -0
- data/config/routes.rb +10 -0
- data/lib/generators/omnitrack/install/install_generator.rb +18 -2
- data/lib/generators/omnitrack/install/templates/create_omnitrack_dashboard_tables.rb +41 -0
- data/lib/generators/omnitrack/install/templates/env.example +6 -0
- data/lib/generators/omnitrack/install/templates/initializer.rb +9 -0
- data/lib/omnitrack/audit/recorder.rb +134 -0
- data/lib/omnitrack/configuration.rb +22 -0
- data/lib/omnitrack/engine.rb +11 -0
- data/lib/omnitrack/middleware/request_tracker.rb +7 -0
- data/lib/omnitrack/railtie.rb +8 -0
- data/lib/omnitrack/version.rb +1 -1
- data/lib/omnitrack.rb +11 -2
- metadata +24 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff7c5f93aedbb1f18f5b24a083661516212f52c8973b2b464ddef8667701de31
|
|
4
|
+
data.tar.gz: dfb4173d07da82b896221c87f3c107b1061eea2d1183d5da16787c5420924235
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 40b53c4312d9986943c0dd6b174b061070731f9a083e3cfd12c171692a6bc11630a68e89a7f013fd8c411dc47e31c3c047f88c8710bf959b5f68870b5c63ff9f
|
|
7
|
+
data.tar.gz: fbb0e26662fd43e79a9d87d7d6e18b9716681ae232c564deec84b4e0adf64f2a4718c5274a85ac30b956329cf850178b090364bfe62d92eb28d47e6b2553e451
|
data/.env.example
CHANGED
|
@@ -41,3 +41,9 @@ TIKTOK_ACCESS_TOKEN=
|
|
|
41
41
|
SNAPCHAT_ENABLED=false
|
|
42
42
|
SNAPCHAT_PIXEL_ID=
|
|
43
43
|
SNAPCHAT_ACCESS_TOKEN=
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# OmniTrack Admin Dashboard
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
OMNITRACK_ADMIN_USERNAME=admin
|
|
49
|
+
OMNITRACK_ADMIN_PASSWORD=change-me
|
data/AI_GEM_SETUP.md
CHANGED
|
@@ -57,6 +57,13 @@ rails generate omnitrack:install
|
|
|
57
57
|
This creates:
|
|
58
58
|
|
|
59
59
|
- `config/initializers/omnitrack.rb`
|
|
60
|
+
- `db/migrate/*_create_omnitrack_dashboard_tables.rb`
|
|
61
|
+
|
|
62
|
+
Then run:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
rails db:migrate
|
|
66
|
+
```
|
|
60
67
|
|
|
61
68
|
**Boot check (must succeed):**
|
|
62
69
|
|
|
@@ -167,6 +174,7 @@ Until `Omnitrack::Jobs::TrackingJob` and ActiveJob are loaded, the gem runs sync
|
|
|
167
174
|
|------|--------|
|
|
168
175
|
| `Gemfile` | `gem "omnitrack-rb"` (and `dotenv-rails` if using `.env`) |
|
|
169
176
|
| `config/initializers/omnitrack.rb` | `Omnitrack.configure` |
|
|
177
|
+
| `db/migrate/*_create_omnitrack_dashboard_tables.rb` | Dashboard history tables migration |
|
|
170
178
|
| `.env.example` | Document variable names (no secrets) — optional if committed |
|
|
171
179
|
| `.env` | Local secrets — **gitignored** |
|
|
172
180
|
| `app/views/layouts/...` | `<%= omnitrack_tags %>` for full-stack |
|
data/README.md
CHANGED
|
@@ -323,6 +323,47 @@ config.active_job.queue_adapter = :sidekiq
|
|
|
323
323
|
|
|
324
324
|
---
|
|
325
325
|
|
|
326
|
+
## Admin Dashboard (Full Visitor History)
|
|
327
|
+
|
|
328
|
+
OmniTrack now includes a dashboard UI at:
|
|
329
|
+
|
|
330
|
+
`/omnitrack/login` (default mount path)
|
|
331
|
+
|
|
332
|
+
Features:
|
|
333
|
+
|
|
334
|
+
- Admin login (username/password)
|
|
335
|
+
- Full visitor lifecycle history (from first request through dispatch)
|
|
336
|
+
- Per-platform delivery status (`success`, `failure`, `queued`, etc.)
|
|
337
|
+
- Event detail page with payload + context snapshots
|
|
338
|
+
- Pagination
|
|
339
|
+
- Arabic/English toggle
|
|
340
|
+
|
|
341
|
+
Setup:
|
|
342
|
+
|
|
343
|
+
1. Run installer and migration:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
rails generate omnitrack:install
|
|
347
|
+
rails db:migrate
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
2. Set dashboard credentials in ENV:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
OMNITRACK_ADMIN_USERNAME=admin
|
|
354
|
+
OMNITRACK_ADMIN_PASSWORD=change-me
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
3. Optional initializer settings:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
config.dashboard_enabled = true
|
|
361
|
+
config.dashboard_per_page = 25
|
|
362
|
+
config.dashboard_mount_path = "/omnitrack" # change per host app if needed
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
326
367
|
## Middleware
|
|
327
368
|
|
|
328
369
|
`Omnitrack::Middleware::RequestTracker` is automatically inserted into your Rack stack. It:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnitrack
|
|
4
|
+
class ApplicationController < ActionController::Base
|
|
5
|
+
layout "omnitrack/admin"
|
|
6
|
+
|
|
7
|
+
before_action :set_locale
|
|
8
|
+
before_action :require_admin_authentication
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def require_admin_authentication
|
|
13
|
+
return if session[:omnitrack_admin_authenticated]
|
|
14
|
+
|
|
15
|
+
redirect_to login_path(locale: I18n.locale), alert: t("omnitrack.auth.login_required")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def set_locale
|
|
19
|
+
requested_locale = params[:locale].presence
|
|
20
|
+
locale = requested_locale&.to_sym
|
|
21
|
+
I18n.locale = I18n.available_locales.include?(locale) ? locale : I18n.default_locale
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnitrack
|
|
4
|
+
module Path
|
|
5
|
+
class ApplicationController < ::Omnitrack::ApplicationController
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def require_admin_authentication
|
|
9
|
+
return if session[:omnitrack_admin_authenticated]
|
|
10
|
+
|
|
11
|
+
redirect_to login_path(locale: I18n.locale), alert: t("omnitrack.auth.login_required")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Omnitrack
|
|
6
|
+
module Path
|
|
7
|
+
class SessionsController < ApplicationController
|
|
8
|
+
skip_before_action :require_admin_authentication, only: %i[new create]
|
|
9
|
+
|
|
10
|
+
def new; end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
if valid_credentials?
|
|
14
|
+
session[:omnitrack_admin_authenticated] = true
|
|
15
|
+
redirect_to visit_events_path(locale: I18n.locale), notice: t("omnitrack.auth.login_success")
|
|
16
|
+
else
|
|
17
|
+
flash.now[:alert] = t("omnitrack.auth.login_failed")
|
|
18
|
+
render :new, status: :unprocessable_entity
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def destroy
|
|
23
|
+
session.delete(:omnitrack_admin_authenticated)
|
|
24
|
+
redirect_to login_path(locale: I18n.locale), notice: t("omnitrack.auth.logout_success")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def valid_credentials?
|
|
30
|
+
secure_compare(params[:username].to_s, Omnitrack.config.admin_username.to_s) &&
|
|
31
|
+
secure_compare(params[:password].to_s, Omnitrack.config.admin_password.to_s)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def secure_compare(value, expected)
|
|
35
|
+
ActiveSupport::SecurityUtils.secure_compare(Digest::SHA256.hexdigest(value), Digest::SHA256.hexdigest(expected))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnitrack
|
|
4
|
+
module Path
|
|
5
|
+
class VisitEventsController < ApplicationController
|
|
6
|
+
def index
|
|
7
|
+
@page = [params.fetch(:page, 1).to_i, 1].max
|
|
8
|
+
@per_page = Omnitrack.config.dashboard_per_page.to_i
|
|
9
|
+
|
|
10
|
+
scoped = Omnitrack::VisitEvent.recent_first
|
|
11
|
+
@total_count = scoped.count
|
|
12
|
+
@total_pages = (@total_count.to_f / @per_page).ceil
|
|
13
|
+
@events = scoped.includes(:delivery_statuses).offset((@page - 1) * @per_page).limit(@per_page)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def show
|
|
17
|
+
@event = Omnitrack::VisitEvent.includes(:delivery_statuses).find(params[:id])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Omnitrack
|
|
6
|
+
class SessionsController < ApplicationController
|
|
7
|
+
skip_before_action :require_admin_authentication, only: %i[new create]
|
|
8
|
+
|
|
9
|
+
def new; end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
if valid_credentials?
|
|
13
|
+
session[:omnitrack_admin_authenticated] = true
|
|
14
|
+
redirect_to visit_events_path(locale: I18n.locale), notice: t("omnitrack.auth.login_success")
|
|
15
|
+
else
|
|
16
|
+
flash.now[:alert] = t("omnitrack.auth.login_failed")
|
|
17
|
+
render :new, status: :unprocessable_entity
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def destroy
|
|
22
|
+
session.delete(:omnitrack_admin_authenticated)
|
|
23
|
+
redirect_to login_path(locale: I18n.locale), notice: t("omnitrack.auth.logout_success")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def valid_credentials?
|
|
29
|
+
secure_compare(params[:username].to_s, Omnitrack.config.admin_username.to_s) &&
|
|
30
|
+
secure_compare(params[:password].to_s, Omnitrack.config.admin_password.to_s)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def secure_compare(value, expected)
|
|
34
|
+
ActiveSupport::SecurityUtils.secure_compare(Digest::SHA256.hexdigest(value), Digest::SHA256.hexdigest(expected))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnitrack
|
|
4
|
+
class VisitEventsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@page = [params.fetch(:page, 1).to_i, 1].max
|
|
7
|
+
@per_page = Omnitrack.config.dashboard_per_page.to_i
|
|
8
|
+
|
|
9
|
+
scoped = Omnitrack::VisitEvent.recent_first
|
|
10
|
+
@total_count = scoped.count
|
|
11
|
+
@total_pages = (@total_count.to_f / @per_page).ceil
|
|
12
|
+
@events = scoped.includes(:delivery_statuses).offset((@page - 1) * @per_page).limit(@per_page)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
@event = Omnitrack::VisitEvent.includes(:delivery_statuses).find(params[:id])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnitrack
|
|
4
|
+
class DeliveryStatus < ApplicationRecord
|
|
5
|
+
self.table_name = "omnitrack_delivery_statuses"
|
|
6
|
+
|
|
7
|
+
belongs_to :visit_event,
|
|
8
|
+
class_name: "Omnitrack::VisitEvent",
|
|
9
|
+
foreign_key: :visit_event_id,
|
|
10
|
+
inverse_of: :delivery_statuses
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnitrack
|
|
4
|
+
class VisitEvent < ApplicationRecord
|
|
5
|
+
self.table_name = "omnitrack_visit_events"
|
|
6
|
+
|
|
7
|
+
has_many :delivery_statuses,
|
|
8
|
+
class_name: "Omnitrack::DeliveryStatus",
|
|
9
|
+
foreign_key: :visit_event_id,
|
|
10
|
+
inverse_of: :visit_event,
|
|
11
|
+
dependent: :delete_all
|
|
12
|
+
|
|
13
|
+
scope :recent_first, -> { order(started_at: :desc) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="<%= I18n.locale %>" dir="<%= I18n.locale.to_s == 'ar' ? 'rtl' : 'ltr' %>">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title><%= t("omnitrack.ui.title") %></title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { --bg:#0f172a; --card:#111827; --text:#f3f4f6; --muted:#9ca3af; --ok:#22c55e; --bad:#ef4444; --warn:#f59e0b; --line:#374151; }
|
|
9
|
+
body { margin:0; background:var(--bg); color:var(--text); font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
10
|
+
.shell { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
|
11
|
+
.top { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:20px; }
|
|
12
|
+
.brand { font-size: 20px; font-weight: 700; }
|
|
13
|
+
.card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:16px; margin-bottom:16px; }
|
|
14
|
+
.grid { display:grid; gap:12px; }
|
|
15
|
+
table { width:100%; border-collapse: collapse; }
|
|
16
|
+
th, td { padding:10px; border-bottom:1px solid var(--line); text-align:start; vertical-align:top; }
|
|
17
|
+
th { color:var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing:.04em; }
|
|
18
|
+
a { color:#93c5fd; text-decoration:none; }
|
|
19
|
+
.badge { border-radius:999px; padding:4px 10px; font-size:12px; font-weight:600; display:inline-block; }
|
|
20
|
+
.success { background: rgba(34,197,94,.15); color: var(--ok); }
|
|
21
|
+
.failure { background: rgba(239,68,68,.15); color: var(--bad); }
|
|
22
|
+
.partial_failure, .processing, .queued, .skipped { background: rgba(245,158,11,.15); color: var(--warn); }
|
|
23
|
+
.muted { color:var(--muted); }
|
|
24
|
+
input, button, select { font: inherit; padding:10px 12px; border-radius:10px; border:1px solid var(--line); background:#0b1220; color:var(--text); }
|
|
25
|
+
button { cursor:pointer; }
|
|
26
|
+
.row { display:flex; gap:10px; align-items:center; flex-wrap: wrap; }
|
|
27
|
+
.alert { padding:10px 12px; border-radius:10px; margin-bottom:14px; }
|
|
28
|
+
.alert-ok { background: rgba(34,197,94,.15); color: var(--ok); }
|
|
29
|
+
.alert-bad { background: rgba(239,68,68,.15); color: var(--bad); }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<div class="shell">
|
|
34
|
+
<div class="top">
|
|
35
|
+
<div class="brand"><%= t("omnitrack.ui.title") %></div>
|
|
36
|
+
<div class="row">
|
|
37
|
+
<%= link_to "EN", params.to_unsafe_h.merge(locale: :en) %>
|
|
38
|
+
<%= link_to "AR", params.to_unsafe_h.merge(locale: :ar) %>
|
|
39
|
+
<% if session[:omnitrack_admin_authenticated] %>
|
|
40
|
+
<%= button_to t("omnitrack.auth.logout"), logout_path(locale: I18n.locale), method: :delete, form_class: "row" %>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<% if flash[:notice] %>
|
|
46
|
+
<div class="alert alert-ok"><%= flash[:notice] %></div>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% if flash[:alert] %>
|
|
49
|
+
<div class="alert alert-bad"><%= flash[:alert] %></div>
|
|
50
|
+
<% end %>
|
|
51
|
+
|
|
52
|
+
<%= yield %>
|
|
53
|
+
</div>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div class="card" style="max-width: 440px; margin: 50px auto;">
|
|
2
|
+
<h2><%= t("omnitrack.auth.login_title") %></h2>
|
|
3
|
+
<p class="muted"><%= t("omnitrack.auth.login_hint") %></p>
|
|
4
|
+
|
|
5
|
+
<%= form_with url: login_submit_path(locale: I18n.locale), method: :post, local: true, class: "grid" do %>
|
|
6
|
+
<label>
|
|
7
|
+
<span><%= t("omnitrack.auth.username") %></span><br>
|
|
8
|
+
<%= text_field_tag :username, nil, required: true, autocomplete: "username" %>
|
|
9
|
+
</label>
|
|
10
|
+
<label>
|
|
11
|
+
<span><%= t("omnitrack.auth.password") %></span><br>
|
|
12
|
+
<%= password_field_tag :password, nil, required: true, autocomplete: "current-password" %>
|
|
13
|
+
</label>
|
|
14
|
+
<button type="submit"><%= t("omnitrack.auth.login_button") %></button>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<div class="row" style="justify-content: space-between;">
|
|
3
|
+
<h2 style="margin:0;"><%= t("omnitrack.events.title") %></h2>
|
|
4
|
+
<div class="muted">
|
|
5
|
+
<%= t("omnitrack.events.total", count: @total_count) %> |
|
|
6
|
+
<%= t("omnitrack.events.page", current: @page, total: [@total_pages, 1].max) %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="card">
|
|
12
|
+
<table>
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<th><%= t("omnitrack.events.columns.id") %></th>
|
|
16
|
+
<th><%= t("omnitrack.events.columns.event") %></th>
|
|
17
|
+
<th><%= t("omnitrack.events.columns.ip") %></th>
|
|
18
|
+
<th><%= t("omnitrack.events.columns.started_at") %></th>
|
|
19
|
+
<th><%= t("omnitrack.events.columns.status") %></th>
|
|
20
|
+
<th><%= t("omnitrack.events.columns.actions") %></th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
<% @events.each do |event| %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td><%= event.id %></td>
|
|
27
|
+
<td><%= event.event_name %></td>
|
|
28
|
+
<td><%= event.ip.presence || "-" %></td>
|
|
29
|
+
<td><%= event.started_at %></td>
|
|
30
|
+
<td><span class="badge <%= event.overall_status %>"><%= event.overall_status %></span></td>
|
|
31
|
+
<td><%= link_to t("omnitrack.events.view"), visit_event_path(event, locale: I18n.locale) %></td>
|
|
32
|
+
</tr>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% if @events.empty? %>
|
|
35
|
+
<tr>
|
|
36
|
+
<td colspan="6" class="muted"><%= t("omnitrack.events.empty") %></td>
|
|
37
|
+
</tr>
|
|
38
|
+
<% end %>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="row">
|
|
44
|
+
<% if @page > 1 %>
|
|
45
|
+
<%= link_to t("omnitrack.events.prev"), visit_events_path(page: @page - 1, locale: I18n.locale) %>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% if @page < @total_pages %>
|
|
48
|
+
<%= link_to t("omnitrack.events.next"), visit_events_path(page: @page + 1, locale: I18n.locale) %>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<div class="row" style="justify-content: space-between;">
|
|
3
|
+
<h2 style="margin:0;"><%= t("omnitrack.events.details_title", id: @event.id) %></h2>
|
|
4
|
+
<%= link_to t("omnitrack.events.back"), visit_events_path(locale: I18n.locale) %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="grid" style="margin-top:12px;">
|
|
8
|
+
<div><strong><%= t("omnitrack.events.columns.event") %>:</strong> <%= @event.event_name %></div>
|
|
9
|
+
<div><strong><%= t("omnitrack.events.columns.status") %>:</strong> <span class="badge <%= @event.overall_status %>"><%= @event.overall_status %></span></div>
|
|
10
|
+
<div><strong><%= t("omnitrack.events.columns.ip") %>:</strong> <%= @event.ip.presence || "-" %></div>
|
|
11
|
+
<div><strong>User Agent:</strong> <%= @event.user_agent.presence || "-" %></div>
|
|
12
|
+
<div><strong>Path:</strong> <%= @event.request_method %> <%= @event.request_path %></div>
|
|
13
|
+
<div><strong>Visitor Token:</strong> <%= @event.visitor_token %></div>
|
|
14
|
+
<div><strong>Started:</strong> <%= @event.started_at %></div>
|
|
15
|
+
<div><strong>Completed:</strong> <%= @event.completed_at %></div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="card">
|
|
20
|
+
<h3><%= t("omnitrack.events.delivery_history") %></h3>
|
|
21
|
+
<table>
|
|
22
|
+
<thead>
|
|
23
|
+
<tr>
|
|
24
|
+
<th>Adapter</th>
|
|
25
|
+
<th><%= t("omnitrack.events.columns.status") %></th>
|
|
26
|
+
<th>Sent At</th>
|
|
27
|
+
<th>Error</th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody>
|
|
31
|
+
<% @event.delivery_statuses.order(sent_at: :asc).each do |delivery| %>
|
|
32
|
+
<tr>
|
|
33
|
+
<td><%= delivery.adapter_name %></td>
|
|
34
|
+
<td><span class="badge <%= delivery.status %>"><%= delivery.status %></span></td>
|
|
35
|
+
<td><%= delivery.sent_at %></td>
|
|
36
|
+
<td><%= delivery.error_message.presence || "-" %></td>
|
|
37
|
+
</tr>
|
|
38
|
+
<% end %>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="card">
|
|
44
|
+
<h3>Payload</h3>
|
|
45
|
+
<pre style="white-space: pre-wrap;"><%= JSON.pretty_generate(@event.payload_json || {}) %></pre>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="card">
|
|
49
|
+
<h3>Context</h3>
|
|
50
|
+
<pre style="white-space: pre-wrap;"><%= JSON.pretty_generate(@event.context_json || {}) %></pre>
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div class="card" style="max-width: 440px; margin: 50px auto;">
|
|
2
|
+
<h2><%= t("omnitrack.auth.login_title") %></h2>
|
|
3
|
+
<p class="muted"><%= t("omnitrack.auth.login_hint") %></p>
|
|
4
|
+
|
|
5
|
+
<%= form_with url: admin_login_submit_path(locale: I18n.locale), method: :post, local: true, class: "grid" do %>
|
|
6
|
+
<label>
|
|
7
|
+
<span><%= t("omnitrack.auth.username") %></span><br>
|
|
8
|
+
<%= text_field_tag :username, nil, required: true, autocomplete: "username" %>
|
|
9
|
+
</label>
|
|
10
|
+
<label>
|
|
11
|
+
<span><%= t("omnitrack.auth.password") %></span><br>
|
|
12
|
+
<%= password_field_tag :password, nil, required: true, autocomplete: "current-password" %>
|
|
13
|
+
</label>
|
|
14
|
+
<button type="submit"><%= t("omnitrack.auth.login_button") %></button>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<div class="row" style="justify-content: space-between;">
|
|
3
|
+
<h2 style="margin:0;"><%= t("omnitrack.events.title") %></h2>
|
|
4
|
+
<div class="muted">
|
|
5
|
+
<%= t("omnitrack.events.total", count: @total_count) %> |
|
|
6
|
+
<%= t("omnitrack.events.page", current: @page, total: [@total_pages, 1].max) %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="card">
|
|
12
|
+
<table>
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<th><%= t("omnitrack.events.columns.id") %></th>
|
|
16
|
+
<th><%= t("omnitrack.events.columns.event") %></th>
|
|
17
|
+
<th><%= t("omnitrack.events.columns.ip") %></th>
|
|
18
|
+
<th><%= t("omnitrack.events.columns.started_at") %></th>
|
|
19
|
+
<th><%= t("omnitrack.events.columns.status") %></th>
|
|
20
|
+
<th><%= t("omnitrack.events.columns.actions") %></th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
<% @events.each do |event| %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td><%= event.id %></td>
|
|
27
|
+
<td><%= event.event_name %></td>
|
|
28
|
+
<td><%= event.ip.presence || "-" %></td>
|
|
29
|
+
<td><%= event.started_at %></td>
|
|
30
|
+
<td><span class="badge <%= event.overall_status %>"><%= event.overall_status %></span></td>
|
|
31
|
+
<td><%= link_to t("omnitrack.events.view"), visit_event_path(event, locale: I18n.locale) %></td>
|
|
32
|
+
</tr>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% if @events.empty? %>
|
|
35
|
+
<tr>
|
|
36
|
+
<td colspan="6" class="muted"><%= t("omnitrack.events.empty") %></td>
|
|
37
|
+
</tr>
|
|
38
|
+
<% end %>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="row">
|
|
44
|
+
<% if @page > 1 %>
|
|
45
|
+
<%= link_to t("omnitrack.events.prev"), visit_events_path(page: @page - 1, locale: I18n.locale) %>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% if @page < @total_pages %>
|
|
48
|
+
<%= link_to t("omnitrack.events.next"), visit_events_path(page: @page + 1, locale: I18n.locale) %>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<div class="row" style="justify-content: space-between;">
|
|
3
|
+
<h2 style="margin:0;"><%= t("omnitrack.events.details_title", id: @event.id) %></h2>
|
|
4
|
+
<%= link_to t("omnitrack.events.back"), visit_events_path(locale: I18n.locale) %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="grid" style="margin-top:12px;">
|
|
8
|
+
<div><strong><%= t("omnitrack.events.columns.event") %>:</strong> <%= @event.event_name %></div>
|
|
9
|
+
<div><strong><%= t("omnitrack.events.columns.status") %>:</strong> <span class="badge <%= @event.overall_status %>"><%= @event.overall_status %></span></div>
|
|
10
|
+
<div><strong><%= t("omnitrack.events.columns.ip") %>:</strong> <%= @event.ip.presence || "-" %></div>
|
|
11
|
+
<div><strong>User Agent:</strong> <%= @event.user_agent.presence || "-" %></div>
|
|
12
|
+
<div><strong>Path:</strong> <%= @event.request_method %> <%= @event.request_path %></div>
|
|
13
|
+
<div><strong>Visitor Token:</strong> <%= @event.visitor_token %></div>
|
|
14
|
+
<div><strong>Started:</strong> <%= @event.started_at %></div>
|
|
15
|
+
<div><strong>Completed:</strong> <%= @event.completed_at %></div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="card">
|
|
20
|
+
<h3><%= t("omnitrack.events.delivery_history") %></h3>
|
|
21
|
+
<table>
|
|
22
|
+
<thead>
|
|
23
|
+
<tr>
|
|
24
|
+
<th>Adapter</th>
|
|
25
|
+
<th><%= t("omnitrack.events.columns.status") %></th>
|
|
26
|
+
<th>Sent At</th>
|
|
27
|
+
<th>Error</th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody>
|
|
31
|
+
<% @event.delivery_statuses.order(sent_at: :asc).each do |delivery| %>
|
|
32
|
+
<tr>
|
|
33
|
+
<td><%= delivery.adapter_name %></td>
|
|
34
|
+
<td><span class="badge <%= delivery.status %>"><%= delivery.status %></span></td>
|
|
35
|
+
<td><%= delivery.sent_at %></td>
|
|
36
|
+
<td><%= delivery.error_message.presence || "-" %></td>
|
|
37
|
+
</tr>
|
|
38
|
+
<% end %>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="card">
|
|
44
|
+
<h3>Payload</h3>
|
|
45
|
+
<pre style="white-space: pre-wrap;"><%= JSON.pretty_generate(@event.payload_json || {}) %></pre>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="card">
|
|
49
|
+
<h3>Context</h3>
|
|
50
|
+
<pre style="white-space: pre-wrap;"><%= JSON.pretty_generate(@event.context_json || {}) %></pre>
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
ar:
|
|
2
|
+
omnitrack:
|
|
3
|
+
ui:
|
|
4
|
+
title: "لوحة أومنِتراك"
|
|
5
|
+
auth:
|
|
6
|
+
login_required: "يرجى تسجيل الدخول كمسؤول أولاً."
|
|
7
|
+
login_success: "تم تسجيل الدخول بنجاح."
|
|
8
|
+
login_failed: "اسم المستخدم أو كلمة المرور غير صحيحة."
|
|
9
|
+
logout_success: "تم تسجيل الخروج بنجاح."
|
|
10
|
+
logout: "تسجيل الخروج"
|
|
11
|
+
login_title: "تسجيل دخول المسؤول"
|
|
12
|
+
login_hint: "استخدم بيانات دخول مسؤول OmniTrack."
|
|
13
|
+
username: "اسم المستخدم"
|
|
14
|
+
password: "كلمة المرور"
|
|
15
|
+
login_button: "دخول"
|
|
16
|
+
events:
|
|
17
|
+
title: "سجل الزائر الكامل"
|
|
18
|
+
details_title: "الحدث رقم %{id}"
|
|
19
|
+
total: "الإجمالي: %{count}"
|
|
20
|
+
page: "الصفحة %{current} / %{total}"
|
|
21
|
+
view: "عرض"
|
|
22
|
+
back: "العودة إلى الأحداث"
|
|
23
|
+
prev: "السابق"
|
|
24
|
+
next: "التالي"
|
|
25
|
+
empty: "لا توجد أحداث بعد."
|
|
26
|
+
delivery_history: "سجل الإرسال للمنصات"
|
|
27
|
+
columns:
|
|
28
|
+
id: "المعرف"
|
|
29
|
+
event: "الحدث"
|
|
30
|
+
ip: "عنوان IP"
|
|
31
|
+
started_at: "وقت البدء"
|
|
32
|
+
status: "الحالة"
|
|
33
|
+
actions: "إجراءات"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
en:
|
|
2
|
+
omnitrack:
|
|
3
|
+
ui:
|
|
4
|
+
title: "OmniTrack Admin"
|
|
5
|
+
auth:
|
|
6
|
+
login_required: "Please login as admin first."
|
|
7
|
+
login_success: "Logged in successfully."
|
|
8
|
+
login_failed: "Invalid username or password."
|
|
9
|
+
logout_success: "Logged out successfully."
|
|
10
|
+
logout: "Logout"
|
|
11
|
+
login_title: "Admin Login"
|
|
12
|
+
login_hint: "Use your OmniTrack admin credentials."
|
|
13
|
+
username: "Username"
|
|
14
|
+
password: "Password"
|
|
15
|
+
login_button: "Login"
|
|
16
|
+
events:
|
|
17
|
+
title: "Visitor Event History"
|
|
18
|
+
details_title: "Event #%{id}"
|
|
19
|
+
total: "Total: %{count}"
|
|
20
|
+
page: "Page %{current} / %{total}"
|
|
21
|
+
view: "View"
|
|
22
|
+
back: "Back to events"
|
|
23
|
+
prev: "Previous"
|
|
24
|
+
next: "Next"
|
|
25
|
+
empty: "No events found yet."
|
|
26
|
+
delivery_history: "Delivery History"
|
|
27
|
+
columns:
|
|
28
|
+
id: "ID"
|
|
29
|
+
event: "Event"
|
|
30
|
+
ip: "IP"
|
|
31
|
+
started_at: "Started At"
|
|
32
|
+
status: "Status"
|
|
33
|
+
actions: "Actions"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Omnitrack::Engine.routes.draw do
|
|
2
|
+
scope module: :path do
|
|
3
|
+
get "login", to: "sessions#new", as: :login
|
|
4
|
+
post "login", to: "sessions#create", as: :login_submit
|
|
5
|
+
delete "logout", to: "sessions#destroy", as: :logout
|
|
6
|
+
resources :visit_events, path: "events", only: %i[index show]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
root to: "path/sessions#new"
|
|
10
|
+
end
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
4
5
|
|
|
5
6
|
module Omnitrack
|
|
6
7
|
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
include Rails::Generators::Migration
|
|
9
|
+
|
|
7
10
|
source_root File.expand_path("templates", __dir__)
|
|
8
11
|
|
|
9
12
|
desc "Creates an Omnitrack initializer and adds it to your application"
|
|
@@ -18,12 +21,23 @@ module Omnitrack
|
|
|
18
21
|
copy_file "env.example", ".env.example"
|
|
19
22
|
end
|
|
20
23
|
|
|
24
|
+
def create_dashboard_migration
|
|
25
|
+
migration_template "create_omnitrack_dashboard_tables.rb",
|
|
26
|
+
"db/migrate/create_omnitrack_dashboard_tables.rb"
|
|
27
|
+
end
|
|
28
|
+
|
|
21
29
|
def show_readme
|
|
22
30
|
readme "README" if behavior == :invoke
|
|
23
31
|
end
|
|
24
32
|
|
|
25
33
|
private
|
|
26
34
|
|
|
35
|
+
def self.next_migration_number(dirname)
|
|
36
|
+
@previous_migration_number ||= current_migration_number(dirname)
|
|
37
|
+
@previous_migration_number = @previous_migration_number.succ
|
|
38
|
+
@previous_migration_number.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
27
41
|
def readme(path)
|
|
28
42
|
say <<~MSG
|
|
29
43
|
|
|
@@ -35,8 +49,10 @@ module Omnitrack
|
|
|
35
49
|
1. Copy .env.example to .env (gitignore .env) and set *_ENABLED + secrets
|
|
36
50
|
2. Optional: add gem "dotenv-rails" in development to load .env
|
|
37
51
|
3. Edit config/initializers/omnitrack.rb
|
|
38
|
-
4.
|
|
39
|
-
5.
|
|
52
|
+
4. Run rails db:migrate to create OmniTrack dashboard tables
|
|
53
|
+
5. Visit /omnitrack/login for the dashboard (or custom dashboard_mount_path)
|
|
54
|
+
6. Place <%= omnitrack_tags %> in your layout <head> (full-stack)
|
|
55
|
+
7. Call Omnitrack.track(...) from controllers/services (see USAGE.md)
|
|
40
56
|
|
|
41
57
|
Full docs: README.md — integration checklist: AI_GEM_SETUP.md
|
|
42
58
|
============================================================
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateOmnitrackDashboardTables < ActiveRecord::Migration[6.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :omnitrack_visit_events do |t|
|
|
6
|
+
t.string :operation
|
|
7
|
+
t.string :event_name
|
|
8
|
+
t.string :request_path
|
|
9
|
+
t.string :request_method
|
|
10
|
+
t.string :ip
|
|
11
|
+
t.string :user_agent
|
|
12
|
+
t.string :session_id
|
|
13
|
+
t.string :visitor_token
|
|
14
|
+
t.string :locale
|
|
15
|
+
t.json :payload_json
|
|
16
|
+
t.json :context_json
|
|
17
|
+
t.datetime :started_at
|
|
18
|
+
t.datetime :completed_at
|
|
19
|
+
t.string :overall_status
|
|
20
|
+
t.timestamps
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
add_index :omnitrack_visit_events, :started_at
|
|
24
|
+
add_index :omnitrack_visit_events, :visitor_token
|
|
25
|
+
add_index :omnitrack_visit_events, :overall_status
|
|
26
|
+
|
|
27
|
+
create_table :omnitrack_delivery_statuses do |t|
|
|
28
|
+
t.references :visit_event, null: false, foreign_key: { to_table: :omnitrack_visit_events }
|
|
29
|
+
t.string :adapter_name
|
|
30
|
+
t.string :status
|
|
31
|
+
t.string :error_message
|
|
32
|
+
t.json :response_json
|
|
33
|
+
t.json :metadata_json
|
|
34
|
+
t.datetime :sent_at
|
|
35
|
+
t.timestamps
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
add_index :omnitrack_delivery_statuses, :status
|
|
39
|
+
add_index :omnitrack_delivery_statuses, :sent_at
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -41,3 +41,9 @@ TIKTOK_ACCESS_TOKEN=
|
|
|
41
41
|
SNAPCHAT_ENABLED=false
|
|
42
42
|
SNAPCHAT_PIXEL_ID=
|
|
43
43
|
SNAPCHAT_ACCESS_TOKEN=
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# OmniTrack Dashboard
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
OMNITRACK_ADMIN_USERNAME=admin
|
|
49
|
+
OMNITRACK_ADMIN_PASSWORD=change-me
|
|
@@ -109,4 +109,13 @@ Omnitrack.configure do |config|
|
|
|
109
109
|
# config.on_error = ->(error, adapter) {
|
|
110
110
|
# Sentry.capture_exception(error, tags: { adapter: adapter.name })
|
|
111
111
|
# }
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Admin Dashboard (full visitor history + platform delivery statuses)
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
config.dashboard_enabled = true
|
|
117
|
+
config.dashboard_per_page = 25
|
|
118
|
+
config.dashboard_mount_path = "/omnitrack"
|
|
119
|
+
config.admin_username = ENV.fetch("OMNITRACK_ADMIN_USERNAME", "admin")
|
|
120
|
+
config.admin_password = ENV.fetch("OMNITRACK_ADMIN_PASSWORD", "change-me")
|
|
112
121
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Omnitrack
|
|
6
|
+
module Audit
|
|
7
|
+
class Recorder
|
|
8
|
+
def self.start(operation, args)
|
|
9
|
+
return NullRecorder.new unless available?
|
|
10
|
+
|
|
11
|
+
new(operation, args)
|
|
12
|
+
rescue StandardError
|
|
13
|
+
NullRecorder.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.available?
|
|
17
|
+
return false unless defined?(ActiveRecord::Base)
|
|
18
|
+
return false unless Omnitrack.config.dashboard_enabled
|
|
19
|
+
|
|
20
|
+
Omnitrack::VisitEvent.table_exists? && Omnitrack::DeliveryStatus.table_exists?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(operation, args)
|
|
24
|
+
@operation = operation.to_s
|
|
25
|
+
@args = args
|
|
26
|
+
@event = create_event!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def finish(result)
|
|
30
|
+
Array(result&.results).each do |adapter_result|
|
|
31
|
+
Omnitrack::DeliveryStatus.create!(
|
|
32
|
+
visit_event_id: @event.id,
|
|
33
|
+
adapter_name: adapter_result.adapter.to_s.presence || "queue",
|
|
34
|
+
status: adapter_result.status.to_s,
|
|
35
|
+
error_message: adapter_result.error&.message,
|
|
36
|
+
response_json: safe_json(adapter_result.data),
|
|
37
|
+
metadata_json: safe_json(adapter_result.metadata),
|
|
38
|
+
sent_at: Time.current
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@event.update!(
|
|
43
|
+
overall_status: overall_status_from(result),
|
|
44
|
+
completed_at: Time.current
|
|
45
|
+
)
|
|
46
|
+
rescue StandardError
|
|
47
|
+
# Never interrupt app flow because of dashboard persistence.
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fail!(error)
|
|
51
|
+
@event&.update!(
|
|
52
|
+
overall_status: "failure",
|
|
53
|
+
completed_at: Time.current,
|
|
54
|
+
context_json: safe_json(base_context.merge(dispatch_error: error.message))
|
|
55
|
+
)
|
|
56
|
+
rescue StandardError
|
|
57
|
+
# no-op
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def create_event!
|
|
63
|
+
Omnitrack::VisitEvent.create!(
|
|
64
|
+
operation: @operation,
|
|
65
|
+
event_name: extract_event_name,
|
|
66
|
+
request_path: request_meta[:path],
|
|
67
|
+
request_method: request_meta[:method],
|
|
68
|
+
ip: context_hash["ip"],
|
|
69
|
+
user_agent: context_hash["user_agent"],
|
|
70
|
+
session_id: request_meta[:session_id],
|
|
71
|
+
visitor_token: request_meta[:visitor_token] || SecureRandom.uuid,
|
|
72
|
+
locale: request_meta[:locale],
|
|
73
|
+
payload_json: safe_json(extract_payload),
|
|
74
|
+
context_json: safe_json(base_context),
|
|
75
|
+
started_at: Time.current,
|
|
76
|
+
overall_status: "processing"
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def request_meta
|
|
81
|
+
@request_meta ||= (context_hash["custom_data"] || {}).transform_keys(&:to_sym)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def base_context
|
|
85
|
+
{
|
|
86
|
+
click_ids: context_hash["click_ids"],
|
|
87
|
+
utm_params: context_hash["utm_params"],
|
|
88
|
+
headers: context_hash["headers"],
|
|
89
|
+
custom_data: context_hash["custom_data"]
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def context_hash
|
|
94
|
+
@context_hash ||= begin
|
|
95
|
+
raw = Omnitrack::Context.current&.to_h || {}
|
|
96
|
+
raw.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def extract_event_name
|
|
101
|
+
return @args.first.to_s if @operation == "track_event"
|
|
102
|
+
return "conversion" if @operation == "track_conversion"
|
|
103
|
+
return "identify" if @operation == "identify_user"
|
|
104
|
+
|
|
105
|
+
@operation
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def extract_payload
|
|
109
|
+
return @args.second || {} if @operation == "track_event"
|
|
110
|
+
|
|
111
|
+
@args.first || {}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def overall_status_from(result)
|
|
115
|
+
return "success" if result.respond_to?(:success?) && result.success?
|
|
116
|
+
return "partial_failure" if result.respond_to?(:any_failure?) && result.any_failure?
|
|
117
|
+
|
|
118
|
+
"queued"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def safe_json(value)
|
|
122
|
+
value.to_h
|
|
123
|
+
rescue StandardError
|
|
124
|
+
value
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class NullRecorder
|
|
129
|
+
def finish(_result); end
|
|
130
|
+
|
|
131
|
+
def fail!(_error); end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -45,6 +45,12 @@ module Omnitrack
|
|
|
45
45
|
# Hook: called with (event_name, adapter_name, error) on adapter failure
|
|
46
46
|
attr_accessor :on_error
|
|
47
47
|
|
|
48
|
+
# Admin dashboard authentication (used by Omnitrack::Engine)
|
|
49
|
+
attr_accessor :admin_username, :admin_password
|
|
50
|
+
|
|
51
|
+
# Admin dashboard behavior
|
|
52
|
+
attr_accessor :dashboard_enabled, :dashboard_per_page, :dashboard_mount_path
|
|
53
|
+
|
|
48
54
|
def initialize
|
|
49
55
|
@mode = :auto
|
|
50
56
|
@adapters = {}
|
|
@@ -57,6 +63,11 @@ module Omnitrack
|
|
|
57
63
|
@max_retries = 3
|
|
58
64
|
@retry_delay = 1
|
|
59
65
|
@on_error = nil
|
|
66
|
+
@admin_username = ENV.fetch("OMNITRACK_ADMIN_USERNAME", "admin")
|
|
67
|
+
@admin_password = ENV.fetch("OMNITRACK_ADMIN_PASSWORD", "change-me")
|
|
68
|
+
@dashboard_enabled = true
|
|
69
|
+
@dashboard_per_page = 25
|
|
70
|
+
@dashboard_mount_path = "/omnitrack"
|
|
60
71
|
end
|
|
61
72
|
|
|
62
73
|
def validate!
|
|
@@ -70,6 +81,17 @@ module Omnitrack
|
|
|
70
81
|
"Invalid log_level: #{@log_level.inspect}. Must be one of #{VALID_LOG_LVLS}"
|
|
71
82
|
end
|
|
72
83
|
|
|
84
|
+
unless @dashboard_per_page.to_i.positive?
|
|
85
|
+
raise Omnitrack::ConfigurationError,
|
|
86
|
+
"dashboard_per_page must be a positive integer"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
mount_path = @dashboard_mount_path.to_s
|
|
90
|
+
unless mount_path.start_with?("/") && mount_path.length > 1
|
|
91
|
+
raise Omnitrack::ConfigurationError,
|
|
92
|
+
"dashboard_mount_path must start with '/' (example: /omnitrack)"
|
|
93
|
+
end
|
|
94
|
+
|
|
73
95
|
true
|
|
74
96
|
end
|
|
75
97
|
|
|
@@ -22,6 +22,13 @@ module Omnitrack
|
|
|
22
22
|
|
|
23
23
|
if Omnitrack.config.auto_capture
|
|
24
24
|
ctx = Omnitrack::Context.from_request(request)
|
|
25
|
+
ctx.merge!(
|
|
26
|
+
path: request.path,
|
|
27
|
+
method: request.request_method,
|
|
28
|
+
session_id: request.session&.id.to_s,
|
|
29
|
+
visitor_token: request.cookies["omnitrack_visitor_token"] || request.session&.id.to_s,
|
|
30
|
+
locale: request.params["locale"]
|
|
31
|
+
)
|
|
25
32
|
Omnitrack::Context.current = ctx
|
|
26
33
|
|
|
27
34
|
Omnitrack.logger.debug("middleware.request",
|
data/lib/omnitrack/railtie.rb
CHANGED
|
@@ -45,6 +45,14 @@ module Omnitrack
|
|
|
45
45
|
Omnitrack.send(:reset_logger!)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
initializer "omnitrack.mount_dashboard_routes" do |app|
|
|
49
|
+
next unless Omnitrack.config.dashboard_enabled
|
|
50
|
+
|
|
51
|
+
app.routes.prepend do
|
|
52
|
+
mount Omnitrack::Engine => Omnitrack.config.dashboard_mount_path, as: :omnitrack
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
48
56
|
# Expose rake tasks
|
|
49
57
|
rake_tasks do
|
|
50
58
|
load File.join(__dir__, "tasks/omnitrack.rake")
|
data/lib/omnitrack/version.rb
CHANGED
data/lib/omnitrack.rb
CHANGED
|
@@ -15,6 +15,7 @@ require_relative "omnitrack/logger"
|
|
|
15
15
|
require_relative "omnitrack/context"
|
|
16
16
|
require_relative "omnitrack/registry"
|
|
17
17
|
require_relative "omnitrack/pipeline/dispatcher"
|
|
18
|
+
require_relative "omnitrack/audit/recorder"
|
|
18
19
|
require_relative "omnitrack/adapters/base"
|
|
19
20
|
require_relative "omnitrack/adapters/google_ads"
|
|
20
21
|
require_relative "omnitrack/adapters/google_analytics"
|
|
@@ -27,6 +28,7 @@ require_relative "omnitrack/concerns/controller"
|
|
|
27
28
|
|
|
28
29
|
# Lazy-load Rails integrations only when Rails is present
|
|
29
30
|
if defined?(Rails)
|
|
31
|
+
require_relative "omnitrack/engine"
|
|
30
32
|
require_relative "omnitrack/railtie"
|
|
31
33
|
require_relative "generators/omnitrack/install/install_generator"
|
|
32
34
|
end
|
|
@@ -136,18 +138,25 @@ module Omnitrack
|
|
|
136
138
|
private
|
|
137
139
|
|
|
138
140
|
def dispatch(operation, *args)
|
|
141
|
+
recorder = Omnitrack::Audit::Recorder.start(operation, args)
|
|
142
|
+
|
|
139
143
|
if config.async && defined?(Omnitrack::Jobs::TrackingJob)
|
|
140
144
|
Omnitrack::Jobs::TrackingJob.perform_later(operation.to_s, *args)
|
|
141
|
-
|
|
145
|
+
result = Omnitrack::MultiResult.new([
|
|
142
146
|
Omnitrack::Result.success(metadata: { queued: true, operation: operation })
|
|
143
147
|
])
|
|
148
|
+
recorder.finish(result)
|
|
149
|
+
return result
|
|
144
150
|
end
|
|
145
151
|
|
|
146
|
-
Omnitrack::Pipeline::Dispatcher.dispatch(operation, *args)
|
|
152
|
+
result = Omnitrack::Pipeline::Dispatcher.dispatch(operation, *args)
|
|
153
|
+
recorder.finish(result)
|
|
154
|
+
result
|
|
147
155
|
rescue StandardError => e
|
|
148
156
|
# Top-level safety net — never raise into the host application
|
|
149
157
|
logger.error("omnitrack.dispatch_error",
|
|
150
158
|
operation: operation, error: e.class.name, message: e.message)
|
|
159
|
+
recorder&.fail!(e)
|
|
151
160
|
Omnitrack::MultiResult.new([
|
|
152
161
|
Omnitrack::Result.failure(error: e)
|
|
153
162
|
])
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omnitrack-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Your Name
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -153,8 +153,28 @@ files:
|
|
|
153
153
|
- LICENSE.txt
|
|
154
154
|
- README.md
|
|
155
155
|
- USAGE.md
|
|
156
|
+
- app/controllers/omnitrack/application_controller.rb
|
|
157
|
+
- app/controllers/omnitrack/path/application_controller.rb
|
|
158
|
+
- app/controllers/omnitrack/path/sessions_controller.rb
|
|
159
|
+
- app/controllers/omnitrack/path/visit_events_controller.rb
|
|
160
|
+
- app/controllers/omnitrack/sessions_controller.rb
|
|
161
|
+
- app/controllers/omnitrack/visit_events_controller.rb
|
|
162
|
+
- app/models/omnitrack/application_record.rb
|
|
163
|
+
- app/models/omnitrack/delivery_status.rb
|
|
164
|
+
- app/models/omnitrack/visit_event.rb
|
|
165
|
+
- app/views/layouts/omnitrack/admin.html.erb
|
|
166
|
+
- app/views/omnitrack/path/sessions/new.html.erb
|
|
167
|
+
- app/views/omnitrack/path/visit_events/index.html.erb
|
|
168
|
+
- app/views/omnitrack/path/visit_events/show.html.erb
|
|
169
|
+
- app/views/omnitrack/sessions/new.html.erb
|
|
170
|
+
- app/views/omnitrack/visit_events/index.html.erb
|
|
171
|
+
- app/views/omnitrack/visit_events/show.html.erb
|
|
172
|
+
- config/locales/ar.yml
|
|
173
|
+
- config/locales/en.yml
|
|
174
|
+
- config/routes.rb
|
|
156
175
|
- lib/generators/omnitrack/install/install_generator.rb
|
|
157
176
|
- lib/generators/omnitrack/install/templates/README
|
|
177
|
+
- lib/generators/omnitrack/install/templates/create_omnitrack_dashboard_tables.rb
|
|
158
178
|
- lib/generators/omnitrack/install/templates/env.example
|
|
159
179
|
- lib/generators/omnitrack/install/templates/initializer.rb
|
|
160
180
|
- lib/omnitrack.rb
|
|
@@ -164,10 +184,12 @@ files:
|
|
|
164
184
|
- lib/omnitrack/adapters/meta.rb
|
|
165
185
|
- lib/omnitrack/adapters/snapchat.rb
|
|
166
186
|
- lib/omnitrack/adapters/tiktok.rb
|
|
187
|
+
- lib/omnitrack/audit/recorder.rb
|
|
167
188
|
- lib/omnitrack/compat/hash_compact_backport.rb
|
|
168
189
|
- lib/omnitrack/concerns/controller.rb
|
|
169
190
|
- lib/omnitrack/configuration.rb
|
|
170
191
|
- lib/omnitrack/context.rb
|
|
192
|
+
- lib/omnitrack/engine.rb
|
|
171
193
|
- lib/omnitrack/errors.rb
|
|
172
194
|
- lib/omnitrack/helpers/view_helpers.rb
|
|
173
195
|
- lib/omnitrack/jobs/tracking_job.rb
|