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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +6 -0
  3. data/AI_GEM_SETUP.md +8 -0
  4. data/README.md +41 -0
  5. data/app/controllers/omnitrack/application_controller.rb +24 -0
  6. data/app/controllers/omnitrack/path/application_controller.rb +15 -0
  7. data/app/controllers/omnitrack/path/sessions_controller.rb +39 -0
  8. data/app/controllers/omnitrack/path/visit_events_controller.rb +21 -0
  9. data/app/controllers/omnitrack/sessions_controller.rb +37 -0
  10. data/app/controllers/omnitrack/visit_events_controller.rb +19 -0
  11. data/app/models/omnitrack/application_record.rb +7 -0
  12. data/app/models/omnitrack/delivery_status.rb +12 -0
  13. data/app/models/omnitrack/visit_event.rb +15 -0
  14. data/app/views/layouts/omnitrack/admin.html.erb +55 -0
  15. data/app/views/omnitrack/path/sessions/new.html.erb +16 -0
  16. data/app/views/omnitrack/path/visit_events/index.html.erb +50 -0
  17. data/app/views/omnitrack/path/visit_events/show.html.erb +51 -0
  18. data/app/views/omnitrack/sessions/new.html.erb +16 -0
  19. data/app/views/omnitrack/visit_events/index.html.erb +50 -0
  20. data/app/views/omnitrack/visit_events/show.html.erb +51 -0
  21. data/config/locales/ar.yml +33 -0
  22. data/config/locales/en.yml +33 -0
  23. data/config/routes.rb +10 -0
  24. data/lib/generators/omnitrack/install/install_generator.rb +18 -2
  25. data/lib/generators/omnitrack/install/templates/create_omnitrack_dashboard_tables.rb +41 -0
  26. data/lib/generators/omnitrack/install/templates/env.example +6 -0
  27. data/lib/generators/omnitrack/install/templates/initializer.rb +9 -0
  28. data/lib/omnitrack/audit/recorder.rb +134 -0
  29. data/lib/omnitrack/configuration.rb +22 -0
  30. data/lib/omnitrack/engine.rb +11 -0
  31. data/lib/omnitrack/middleware/request_tracker.rb +7 -0
  32. data/lib/omnitrack/railtie.rb +8 -0
  33. data/lib/omnitrack/version.rb +1 -1
  34. data/lib/omnitrack.rb +11 -2
  35. metadata +24 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa0f487b57d4c47c6a3168bb0b2583982926b1361f30d95a4d349f3a3bae9834
4
- data.tar.gz: 2c5c0967326204f7e9e83f5e43b80c05ee97b1ea414af87ada2fb785b11d2211
3
+ metadata.gz: ff7c5f93aedbb1f18f5b24a083661516212f52c8973b2b464ddef8667701de31
4
+ data.tar.gz: dfb4173d07da82b896221c87f3c107b1061eea2d1183d5da16787c5420924235
5
5
  SHA512:
6
- metadata.gz: a27d997c0372cb36c36c4c84816739c66ddf886b6953985ebb27f940fb6f2babf5f2111e8d36555aae09e90c8a10138478d0ba6fc0eca79c11dbc6476c2234fa
7
- data.tar.gz: c1bb1d889f61bfdd656ab0ede5b7d2ccc7b432fcf4b7db335c63789d709736caa8dd3cb1d8fe614ec5c7214e46fe28c8cc85ba306fb0cef69b82c48c9ca9db60
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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnitrack
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ 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. Place <%= omnitrack_tags %> in your layout <head> (full-stack)
39
- 5. Call Omnitrack.track(...) from controllers/services (see USAGE.md)
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
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnitrack
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Omnitrack
6
+
7
+ initializer "omnitrack.load_i18n" do |app|
8
+ app.config.i18n.load_path += Dir[root.join("config", "locales", "*.yml")]
9
+ end
10
+ end
11
+ end
@@ -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",
@@ -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")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnitrack
4
- VERSION = "2.0.0"
4
+ VERSION = "3.0.0"
5
5
  end
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
- return Omnitrack::MultiResult.new([
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: 2.0.0
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-26 00:00:00.000000000 Z
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