omnitrack-rb 0.1.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.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnitrack
4
+ # Railtie hooks OmniTrack into Rails automatically.
5
+ # No user action required — simply add the gem and run the installer.
6
+ class Railtie < Rails::Railtie
7
+ # Allow Rails config namespace: config.omnitrack.mode = :backend
8
+ config.omnitrack = ActiveSupport::OrderedOptions.new
9
+
10
+ initializer "omnitrack.tracking_job" do
11
+ if defined?(ActiveJob::Base)
12
+ require_relative "jobs/tracking_job"
13
+ end
14
+ end
15
+
16
+ initializer "omnitrack.configure" do |app|
17
+ # Map any rails config keys into Omnitrack config
18
+ rails_cfg = app.config.omnitrack
19
+ if rails_cfg.any?
20
+ Omnitrack.configure do |c|
21
+ c.mode = rails_cfg[:mode] if rails_cfg[:mode]
22
+ c.log_level = rails_cfg[:log_level] if rails_cfg[:log_level]
23
+ c.async = rails_cfg[:async] if rails_cfg.key?(:async)
24
+ end
25
+ end
26
+ end
27
+
28
+ initializer "omnitrack.middleware" do |app|
29
+ app.middleware.use Omnitrack::Middleware::RequestTracker
30
+ end
31
+
32
+ initializer "omnitrack.view_helpers" do
33
+ ActiveSupport.on_load(:action_view) do
34
+ include Omnitrack::Helpers::ViewHelpers
35
+ end
36
+ end
37
+
38
+ initializer "omnitrack.controller_helpers" do
39
+ ActiveSupport.on_load(:action_controller) do
40
+ include Omnitrack::Controller
41
+ end
42
+ end
43
+
44
+ initializer "omnitrack.logger_setup" do
45
+ Omnitrack.send(:reset_logger!)
46
+ end
47
+
48
+ # Expose rake tasks
49
+ rake_tasks do
50
+ load File.join(__dir__, "tasks/omnitrack.rake")
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnitrack
4
+ # Thread-safe registry mapping adapter names (symbols) → adapter classes.
5
+ # Adapters are auto-registered via Base.inherited and can also be registered
6
+ # manually with Registry.register.
7
+ module Registry
8
+ MUTEX = Mutex.new
9
+ private_constant :MUTEX
10
+
11
+ @adapters = {}
12
+
13
+ class << self
14
+ # Register an adapter class.
15
+ # @param klass [Class] must inherit from Omnitrack::Adapters::Base
16
+ def register(klass)
17
+ MUTEX.synchronize do
18
+ @adapters[klass.adapter_name] = klass
19
+ end
20
+ end
21
+
22
+ # Look up an adapter class by name.
23
+ # @param name [Symbol, String]
24
+ # @return [Class]
25
+ # @raise [Omnitrack::UnknownAdapterError]
26
+ def lookup(name)
27
+ klass = MUTEX.synchronize { @adapters[name.to_sym] }
28
+ raise Omnitrack::UnknownAdapterError, "Unknown adapter: #{name}" unless klass
29
+
30
+ klass
31
+ end
32
+
33
+ # All registered adapter names
34
+ def registered
35
+ MUTEX.synchronize { @adapters.keys }
36
+ end
37
+
38
+ # Instantiate all *enabled* adapters based on current configuration.
39
+ # @return [Array<Omnitrack::Adapters::Base>]
40
+ def enabled_adapters
41
+ MUTEX.synchronize { @adapters.dup }.filter_map do |adapter_name, klass|
42
+ cfg = Omnitrack.config.adapter_config(adapter_name)
43
+ next unless cfg.fetch(:enabled, false)
44
+
45
+ klass.new(config: cfg, logger: Omnitrack.logger)
46
+ rescue Omnitrack::ConfigurationError => e
47
+ Omnitrack.logger.error("registry.init_error",
48
+ adapter: adapter_name, message: e.message)
49
+ nil
50
+ end
51
+ end
52
+
53
+ # Clear all registrations (useful in tests)
54
+ def reset!
55
+ MUTEX.synchronize { @adapters = {} }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnitrack
4
+ # Immutable value object returned by every adapter call.
5
+ # Provides a consistent interface regardless of platform.
6
+ class Result
7
+ attr_reader :status, :adapter, :data, :error, :metadata
8
+
9
+ def initialize(status:, adapter: nil, data: nil, error: nil, metadata: {})
10
+ @status = status.to_sym # :success | :failure | :skipped
11
+ @adapter = adapter
12
+ @data = data
13
+ @error = error
14
+ @metadata = metadata || {}
15
+ freeze
16
+ end
17
+
18
+ # -------------------------------------------------------------------
19
+ # Factory methods
20
+ # -------------------------------------------------------------------
21
+
22
+ def self.success(adapter: nil, data: nil, metadata: {})
23
+ new(status: :success, adapter: adapter, data: data, metadata: metadata)
24
+ end
25
+
26
+ def self.failure(error: nil, adapter: nil, metadata: {})
27
+ new(status: :failure, adapter: adapter, error: error, metadata: metadata)
28
+ end
29
+
30
+ def self.skipped(reason: nil, adapter: nil)
31
+ new(status: :skipped, adapter: adapter,
32
+ metadata: { reason: reason })
33
+ end
34
+
35
+ # -------------------------------------------------------------------
36
+ # Predicate helpers
37
+ # -------------------------------------------------------------------
38
+
39
+ def success?
40
+ @status == :success
41
+ end
42
+
43
+ def failure?
44
+ @status == :failure
45
+ end
46
+
47
+ def skipped?
48
+ @status == :skipped
49
+ end
50
+
51
+ # -------------------------------------------------------------------
52
+ # Serialisation
53
+ # -------------------------------------------------------------------
54
+
55
+ def to_h
56
+ {
57
+ status: @status,
58
+ adapter: @adapter,
59
+ data: @data,
60
+ error: @error&.message,
61
+ metadata: @metadata
62
+ }
63
+ end
64
+
65
+ def to_json(*args)
66
+ require "json"
67
+ JSON.generate(to_h, *args)
68
+ end
69
+
70
+ def inspect
71
+ "#<Omnitrack::Result status=#{@status} adapter=#{@adapter} " \
72
+ "error=#{@error&.message.inspect}>"
73
+ end
74
+ end
75
+
76
+ # Aggregated result for pipeline dispatches (one result per adapter)
77
+ class MultiResult
78
+ include Enumerable
79
+
80
+ attr_reader :results
81
+
82
+ def initialize(results = [])
83
+ @results = results.freeze
84
+ end
85
+
86
+ def each(&block)
87
+ @results.each(&block)
88
+ end
89
+
90
+ def success?
91
+ @results.all?(&:success?)
92
+ end
93
+
94
+ def any_failure?
95
+ @results.any?(&:failure?)
96
+ end
97
+
98
+ def failures
99
+ @results.select(&:failure?)
100
+ end
101
+
102
+ def successes
103
+ @results.select(&:success?)
104
+ end
105
+
106
+ def to_h
107
+ {
108
+ success: success?,
109
+ results: @results.map(&:to_h)
110
+ }
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :omnitrack do
4
+ desc "Show OmniTrack configuration and registered adapters"
5
+ task status: :environment do
6
+ puts "\n=== OmniTrack v#{Omnitrack::VERSION} ===\n"
7
+ puts "Mode: #{Omnitrack.config.effective_mode}"
8
+ puts "Log file: #{Omnitrack.config.resolved_log_file}"
9
+ puts "Log level: #{Omnitrack.config.log_level}"
10
+ puts "Async: #{Omnitrack.config.async}"
11
+ puts "Queue: #{Omnitrack.config.queue_name}"
12
+ puts "\nRegistered adapters:"
13
+ Omnitrack::Registry.registered.each do |adapter_name|
14
+ enabled = Omnitrack.config.adapter_enabled?(adapter_name)
15
+ status = enabled ? "✓ enabled" : "✗ disabled"
16
+ puts " #{adapter_name.to_s.ljust(20)} #{status}"
17
+ end
18
+ puts ""
19
+ end
20
+
21
+ desc "Send a test event through all enabled adapters"
22
+ task :test_event, [:event_name] => :environment do |_, args|
23
+ event = args[:event_name] || "test_event"
24
+ puts "Sending test event '#{event}' through all enabled adapters..."
25
+ result = Omnitrack.track(event, value: 1.0, currency: "USD", test: true)
26
+ result.each do |r|
27
+ icon = r.success? ? "✓" : (r.skipped? ? "–" : "✗")
28
+ puts " #{icon} #{r.adapter}: #{r.status}"
29
+ puts " error: #{r.error&.message}" if r.failure?
30
+ end
31
+ puts ""
32
+ end
33
+
34
+ desc "Rotate OmniTrack log file"
35
+ task rotate_log: :environment do
36
+ Omnitrack.logger.reopen
37
+ puts "OmniTrack log rotated: #{Omnitrack.config.resolved_log_file}"
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnitrack
4
+ VERSION = "0.1.0"
5
+ end
data/lib/omnitrack.rb ADDED
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/object/blank"
5
+ require "active_support/core_ext/string/inflections"
6
+ require "fileutils"
7
+ require "securerandom"
8
+
9
+ require_relative "omnitrack/version"
10
+ require_relative "omnitrack/errors"
11
+ require_relative "omnitrack/result"
12
+ require_relative "omnitrack/configuration"
13
+ require_relative "omnitrack/logger"
14
+ require_relative "omnitrack/context"
15
+ require_relative "omnitrack/registry"
16
+ require_relative "omnitrack/pipeline/dispatcher"
17
+ require_relative "omnitrack/adapters/base"
18
+ require_relative "omnitrack/adapters/google_ads"
19
+ require_relative "omnitrack/adapters/google_analytics"
20
+ require_relative "omnitrack/adapters/meta"
21
+ require_relative "omnitrack/adapters/tiktok"
22
+ require_relative "omnitrack/adapters/snapchat"
23
+ require_relative "omnitrack/middleware/request_tracker"
24
+ require_relative "omnitrack/helpers/view_helpers"
25
+ require_relative "omnitrack/concerns/controller"
26
+
27
+ # Lazy-load Rails integrations only when Rails is present
28
+ if defined?(Rails)
29
+ require_relative "omnitrack/railtie"
30
+ require_relative "generators/omnitrack/install/install_generator"
31
+ end
32
+
33
+ # ==============================================================================
34
+ # Omnitrack
35
+ # ==============================================================================
36
+ # Top-level module providing the primary public API.
37
+ #
38
+ # Usage:
39
+ # Omnitrack.configure { |c| c.adapters = { ... } }
40
+ # Omnitrack.track("purchase", value: 99.00, currency: "USD")
41
+ # Omnitrack.track_conversion(value: 99.00, order_id: "ORD-001")
42
+ # Omnitrack.identify(email: "user@example.com")
43
+ #
44
+ module Omnitrack
45
+ class << self
46
+ # -------------------------------------------------------------------
47
+ # Configuration
48
+ # -------------------------------------------------------------------
49
+
50
+ # Access the global configuration object.
51
+ # @return [Omnitrack::Configuration]
52
+ def config
53
+ @config ||= Configuration.new
54
+ end
55
+
56
+ # Configure OmniTrack via block.
57
+ #
58
+ # Omnitrack.configure do |c|
59
+ # c.mode = :hybrid
60
+ # c.adapters = { meta: { enabled: true, pixel_id: "..." } }
61
+ # end
62
+ #
63
+ def configure
64
+ yield config
65
+ config.validate!
66
+ reset_logger!
67
+ self
68
+ end
69
+
70
+ # Reset configuration and logger (useful in tests)
71
+ def reset!
72
+ @config = nil
73
+ @logger = nil
74
+ Omnitrack::Registry.reset!
75
+ # Re-bind built-in adapters without reloading (avoids duplicate constant/method warnings)
76
+ [
77
+ Omnitrack::Adapters::GoogleAds,
78
+ Omnitrack::Adapters::GoogleAnalytics,
79
+ Omnitrack::Adapters::Meta,
80
+ Omnitrack::Adapters::TikTok,
81
+ Omnitrack::Adapters::Snapchat
82
+ ].each { |adapter_class| Omnitrack::Registry.register(adapter_class) }
83
+ end
84
+
85
+ # -------------------------------------------------------------------
86
+ # Logger
87
+ # -------------------------------------------------------------------
88
+
89
+ # @return [Omnitrack::Logger]
90
+ def logger
91
+ @logger ||= build_logger
92
+ end
93
+
94
+ # -------------------------------------------------------------------
95
+ # Public tracking API
96
+ # -------------------------------------------------------------------
97
+
98
+ # Track a named event across all enabled adapters.
99
+ #
100
+ # @param event_name [String, Symbol]
101
+ # @param payload [Hash]
102
+ # @return [Omnitrack::MultiResult]
103
+ def track(event_name, payload = {})
104
+ dispatch(:track_event, event_name.to_s, payload.to_h)
105
+ end
106
+
107
+ # Track a conversion across all enabled adapters.
108
+ #
109
+ # @param data [Hash]
110
+ # @return [Omnitrack::MultiResult]
111
+ def track_conversion(data = {})
112
+ dispatch(:track_conversion, data.to_h)
113
+ end
114
+
115
+ # Identify a user across all enabled adapters.
116
+ #
117
+ # @param user_data [Hash] keys: :email, :phone, :external_id, :first_name, :last_name, etc.
118
+ # @return [Omnitrack::MultiResult]
119
+ def identify(user_data = {})
120
+ dispatch(:identify_user, user_data.to_h)
121
+ end
122
+
123
+ # -------------------------------------------------------------------
124
+ # Mode helpers
125
+ # -------------------------------------------------------------------
126
+
127
+ def frontend_mode?
128
+ %i[frontend hybrid].include?(config.effective_mode)
129
+ end
130
+
131
+ def backend_mode?
132
+ %i[backend hybrid].include?(config.effective_mode)
133
+ end
134
+
135
+ private
136
+
137
+ def dispatch(operation, *args)
138
+ if config.async && defined?(Omnitrack::Jobs::TrackingJob)
139
+ Omnitrack::Jobs::TrackingJob.perform_later(operation.to_s, *args)
140
+ return Omnitrack::MultiResult.new([
141
+ Omnitrack::Result.success(metadata: { queued: true, operation: operation })
142
+ ])
143
+ end
144
+
145
+ Omnitrack::Pipeline::Dispatcher.dispatch(operation, *args)
146
+ rescue StandardError => e
147
+ # Top-level safety net — never raise into the host application
148
+ logger.error("omnitrack.dispatch_error",
149
+ operation: operation, error: e.class.name, message: e.message)
150
+ Omnitrack::MultiResult.new([
151
+ Omnitrack::Result.failure(error: e)
152
+ ])
153
+ end
154
+
155
+ def build_logger
156
+ Omnitrack::Logger.new(
157
+ log_file: config.resolved_log_file,
158
+ log_level: config.log_level
159
+ )
160
+ end
161
+
162
+ def reset_logger!
163
+ @logger = nil
164
+ end
165
+ end
166
+ end
metadata ADDED
@@ -0,0 +1,207 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omnitrack-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Your Name
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: net-http
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '13.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '13.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.12'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.12'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rails
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '6.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '6.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.50'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.50'
139
+ description: |
140
+ OmniTrack is a production-grade tracking and conversion gem for Ruby on Rails.
141
+ It supports multiple ad/analytics platforms (Google Ads, GA4, Meta, TikTok, Snapchat)
142
+ via a clean adapter pattern, works in both full-stack and API-only Rails apps,
143
+ and provides structured logging, background job support, and an extensible pipeline.
144
+ email:
145
+ - you@example.com
146
+ executables: []
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - ".env.example"
151
+ - AI_GEM_SETUP.md
152
+ - CHANGELOG.md
153
+ - LICENSE.txt
154
+ - README.md
155
+ - USAGE.md
156
+ - lib/generators/omnitrack/install/install_generator.rb
157
+ - lib/generators/omnitrack/install/templates/README
158
+ - lib/generators/omnitrack/install/templates/env.example
159
+ - lib/generators/omnitrack/install/templates/initializer.rb
160
+ - lib/omnitrack.rb
161
+ - lib/omnitrack/adapters/base.rb
162
+ - lib/omnitrack/adapters/google_ads.rb
163
+ - lib/omnitrack/adapters/google_analytics.rb
164
+ - lib/omnitrack/adapters/meta.rb
165
+ - lib/omnitrack/adapters/snapchat.rb
166
+ - lib/omnitrack/adapters/tiktok.rb
167
+ - lib/omnitrack/concerns/controller.rb
168
+ - lib/omnitrack/configuration.rb
169
+ - lib/omnitrack/context.rb
170
+ - lib/omnitrack/errors.rb
171
+ - lib/omnitrack/helpers/view_helpers.rb
172
+ - lib/omnitrack/jobs/tracking_job.rb
173
+ - lib/omnitrack/logger.rb
174
+ - lib/omnitrack/middleware/request_tracker.rb
175
+ - lib/omnitrack/pipeline/dispatcher.rb
176
+ - lib/omnitrack/railtie.rb
177
+ - lib/omnitrack/registry.rb
178
+ - lib/omnitrack/result.rb
179
+ - lib/omnitrack/tasks/omnitrack.rake
180
+ - lib/omnitrack/version.rb
181
+ homepage: https://github.com/yourorg/omnitrack-rb
182
+ licenses:
183
+ - MIT
184
+ metadata:
185
+ homepage_uri: https://github.com/yourorg/omnitrack-rb
186
+ source_code_uri: https://github.com/yourorg/omnitrack-rb
187
+ changelog_uri: https://github.com/yourorg/omnitrack-rb/blob/main/CHANGELOG.md
188
+ post_install_message:
189
+ rdoc_options: []
190
+ require_paths:
191
+ - lib
192
+ required_ruby_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: 2.7.0
197
+ required_rubygems_version: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ requirements: []
203
+ rubygems_version: 3.4.10
204
+ signing_key:
205
+ specification_version: 4
206
+ summary: Flexible, modular tracking and conversion system for Rails
207
+ test_files: []