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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3159ca70c1926bd7576f69daa82b043b7712219c920ad13c956560294b52940c
4
+ data.tar.gz: 8f86771d6bba1b2c6531b1076c369eb464faa40406d111aacebe0610273f83df
5
+ SHA512:
6
+ metadata.gz: d49fed2d40c06182c33922feafdf8fb7969480c3468d03b95248594fb16517ce0e6f8cc3171c002bd3d8b83ce9fa097d0f61bb88bdf7156015cfa10ea50d16de
7
+ data.tar.gz: d95277587d7eab7b07e4cebbad26e593d96ee00dafff1ebcbcc45953d073db1255c6b27585011e61b006131078c93814d6e530e4986660a6d0c7cd692efef4db
data/.env.example ADDED
@@ -0,0 +1,43 @@
1
+ # OmniTrack (omnitrack-rb) — copy to .env in your Rails app and fill real values.
2
+ # Never commit .env. Use "cp .env.example .env" and enable only what you need.
3
+ #
4
+ # Enable flags: set to the string "true" to turn an adapter on (see config/initializers/omnitrack.rb).
5
+
6
+ # ---------------------------------------------------------------------------
7
+ # Google Ads (server: Click Conversion Upload API)
8
+ # ---------------------------------------------------------------------------
9
+ GOOGLE_ADS_ENABLED=false
10
+ GOOGLE_ADS_CUSTOMER_ID=
11
+ GOOGLE_ADS_DEVELOPER_TOKEN=
12
+ GOOGLE_ADS_ACCESS_TOKEN=
13
+ GOOGLE_ADS_CONVERSION_ACTION_ID=
14
+ # GOOGLE_ADS_LOGIN_CUSTOMER_ID= # MCC / manager account only
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Google Analytics 4 (server: Measurement Protocol; browser: gtag in layout)
18
+ # ---------------------------------------------------------------------------
19
+ GA4_ENABLED=false
20
+ GA4_MEASUREMENT_ID=
21
+ GA4_API_SECRET=
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Meta (Facebook / Instagram) Conversions API + pixel
25
+ # ---------------------------------------------------------------------------
26
+ META_ENABLED=false
27
+ META_PIXEL_ID=
28
+ META_ACCESS_TOKEN=
29
+ # META_TEST_EVENT_CODE= # optional: Events Manager test tool
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # TikTok Ads
33
+ # ---------------------------------------------------------------------------
34
+ TIKTOK_ENABLED=false
35
+ TIKTOK_PIXEL_ID=
36
+ TIKTOK_ACCESS_TOKEN=
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Snapchat Ads
40
+ # ---------------------------------------------------------------------------
41
+ SNAPCHAT_ENABLED=false
42
+ SNAPCHAT_PIXEL_ID=
43
+ SNAPCHAT_ACCESS_TOKEN=
data/AI_GEM_SETUP.md ADDED
@@ -0,0 +1,164 @@
1
+ # OmniTrack-rb — integration guide for tools and maintainers
2
+
3
+ Use this document when **adding `omnitrack-rb` to an existing Rails 6+ app** so the project boots, initializers run, and ENV is wired with **no runtime errors** from the gem. Follow the steps in order.
4
+
5
+ ---
6
+
7
+ ## 1. Gemfile (required)
8
+
9
+ In the **host** application `Gemfile`:
10
+
11
+ ```ruby
12
+ gem "omnitrack-rb"
13
+ ```
14
+
15
+ Then run:
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ **Do not** add a `require` in `config/application.rb` for this gem; the Railtie loads with Rails when Bundler requires the default group.
22
+
23
+ ---
24
+
25
+ ## 2. Install initializer (required)
26
+
27
+ From the app root:
28
+
29
+ ```bash
30
+ rails generate omnitrack:install
31
+ ```
32
+
33
+ This creates:
34
+
35
+ - `config/initializers/omnitrack.rb`
36
+
37
+ **Boot check (must succeed):**
38
+
39
+ ```bash
40
+ bin/rails runner "puts Omnitrack::VERSION"
41
+ ```
42
+
43
+ If this fails, fix Bundler / Ruby version and ensure `bundle install` completed.
44
+
45
+ ---
46
+
47
+ ## 3. Environment variables (no secrets in git)
48
+
49
+ The generated initializer uses `ENV[...]` for all credentials. **All adapters are off until you set each `*_ENABLED=true`.** That avoids accidental API calls with empty keys.
50
+
51
+ ### Option A — `.env` in development (recommended for local work)
52
+
53
+ 1. Add to **host** `Gemfile` (development and test; optional for other groups):
54
+
55
+ ```ruby
56
+ group :development, :test do
57
+ gem "dotenv-rails"
58
+ end
59
+ ```
60
+
61
+ 2. `bundle install`
62
+
63
+ 3. If the install generator did not create it, copy the example file in the app root:
64
+
65
+ ```bash
66
+ cp .env.example .env
67
+ ```
68
+
69
+ (Running `rails generate omnitrack:install` creates `.env.example` in the app root when it does not already exist. You can also copy from the gem: `bundle show omnitrack-rb` then open that directory’s `.env.example`.)
70
+
71
+ 4. Add **host** `.gitignore` line if missing:
72
+
73
+ ```
74
+ .env
75
+ .env.*.local
76
+ ```
77
+
78
+ 5. In `.env`, set only the platforms you use. Start with all `*_ENABLED=false` and flip one to `true` when credentials are real.
79
+
80
+ 6. **Load order:** `dotenv-rails` runs before `config/initializers/*`, so `ENV` is set when `omnitrack.rb` runs.
81
+
82
+ ### Option B — Production / staging (no .env file)
83
+
84
+ Set the same variable names in your host (Kubernetes secrets, Heroku config, 1Password, Doppler, etc.). **Never** require `.env` in production; the app reads the real environment.
85
+
86
+ ### Option C — `Rails credentials` (optional)
87
+
88
+ If you use encrypted credentials, read values in the initializer and assign them before `Omnitrack.configure`, or set `ENV` in `config/application.rb` from `Rails.application.credentials` — keep adapter keys out of version control.
89
+
90
+ ---
91
+
92
+ ## 4. ActiveJob and async (avoid confusion)
93
+
94
+ `config/initializers/omnitrack.rb` (generated) may set `config.async = Rails.env.production?`.
95
+
96
+ - If `async` is **true** in an environment, **ActiveJob** must be usable and a queue adapter (e.g. Sidekiq) should be set for production.
97
+ - If you are not ready for jobs, set in the initializer:
98
+
99
+ ```ruby
100
+ config.async = false
101
+ ```
102
+
103
+ Until `Omnitrack::Jobs::TrackingJob` and ActiveJob are loaded, the gem runs synchronously when `async` is false (safe default for first integration).
104
+
105
+ ---
106
+
107
+ ## 5. Full-stack: layout and controller (one-time)
108
+
109
+ - **Layout head** — `app/views/layouts/application.html.erb` (or your main layout) inside `<head>`:
110
+
111
+ ```erb
112
+ <%= omnitrack_tags %>
113
+ ```
114
+
115
+ - **Controller** — call tracking **after** a successful business outcome (e.g. order created). The Railtie already includes `Omnitrack::Controller` on `ActionController` — you can use `omnitrack_event` / `Omnitrack.track` in actions. See [USAGE.md](USAGE.md#8-concrete-examples-files-and-line-where-to-put-calls) for file-level examples.
116
+
117
+ ---
118
+
119
+ ## 6. API-only apps
120
+
121
+ - No `omnitrack_tags` in layout (optional to omit).
122
+ - Pass `gclid`, `fbclid`, `ttclid` from the client in params or body; the middleware and `Omnitrack::Context` still apply when the request includes them.
123
+ - `config.mode` default `:auto` will treat `api_only` apps as **backend**-oriented (no browser pixels unless you set `:hybrid` / `:frontend` on purpose).
124
+
125
+ ---
126
+
127
+ ## 7. Common mistakes (avoid “mysterious” errors)
128
+
129
+ | Symptom | What to do |
130
+ |--------|------------|
131
+ | `uninitialized constant Omnitrack` | Run `bundle install`; ensure `config/application.rb` loads Bundler default group; boot with `bin/rails c`. |
132
+ | `ENV[...] is nil` in logs | Adapters with `enabled: true` but missing tokens may log or skip; set `*_ENABLED=false` until credentials exist. |
133
+ | Job errors when `async: true` | Set `config.async = false` until ActiveJob and queue are configured, or add queue adapter. |
134
+ | `.env` not applied | Add `dotenv-rails` in **development**; restart Spring/server; do not commit `.env`. |
135
+ | Clicks not attributed | Pass click IDs in request params/headers; ensure `config.auto_capture = true` (default in template). |
136
+
137
+ ---
138
+
139
+ ## 8. Files the host app should have after a clean setup
140
+
141
+ | Path | Purpose |
142
+ |------|--------|
143
+ | `Gemfile` | `gem "omnitrack-rb"` (and `dotenv-rails` if using `.env`) |
144
+ | `config/initializers/omnitrack.rb` | `Omnitrack.configure` |
145
+ | `.env.example` | Document variable names (no secrets) — optional if committed |
146
+ | `.env` | Local secrets — **gitignored** |
147
+ | `app/views/layouts/...` | `<%= omnitrack_tags %>` for full-stack |
148
+ | `log/omnitrack.log` | Created at runtime when the logger writes (ensure `log/` is writable) |
149
+
150
+ ---
151
+
152
+ ## 9. Smoke test after install
153
+
154
+ ```bash
155
+ bin/rails omnitrack:status
156
+ ```
157
+
158
+ Enable one adapter in `.env` with valid test credentials, then (carefully) run:
159
+
160
+ ```bash
161
+ bin/rails "omnitrack:test_event[purchase]"
162
+ ```
163
+
164
+ For day-to-day **method placement** (which file, which line pattern), use [USAGE.md](USAGE.md).
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+ - `AI_GEM_SETUP.md` — step-by-step integration for host apps (Bundler, initializer, ENV, ActiveJob, pitfalls)
7
+ - `.env.example` — variable names matching the install template; install generator copies `env.example` → `.env.example` when missing
8
+ - `USAGE.md` — concrete ERB/Ruby examples for layout, HTML/API controllers, and a service object
9
+
10
+ ### Fixed
11
+ - Conventional `lib/omnitrack` gem layout, loadable `require "omnitrack"`
12
+ - `rails generate omnitrack:install` generator at `lib/generators/omnitrack/install/`
13
+ - `Omnitrack::Controller` as an alias of `Omnitrack::Concerns::Controller`
14
+ - Default `adapter_name` for adapters uses `underscore` (snake_case) so config keys (e.g. `google_ads`) match the registry; `Omnitrack.reset!` re-registers first-party adapters in tests
15
+ - `activesupport` as a direct runtime dependency; explicit requires for `ActiveSupport` extensions used at load time
16
+
17
+ ## [0.1.0] - 2024-01-15
18
+
19
+ ### Added
20
+ - Initial release
21
+ - Adapter-based architecture with `Omnitrack::Adapters::Base`
22
+ - Google Ads adapter (Click Conversion Upload API)
23
+ - Google Analytics 4 adapter (Measurement Protocol)
24
+ - Meta adapter (Conversions API)
25
+ - TikTok adapter (Events API)
26
+ - Snapchat adapter (Conversions API)
27
+ - Rack middleware for automatic click ID / UTM capture
28
+ - Thread-local `Omnitrack::Context` for request data
29
+ - Structured JSON logging to `log/omnitrack.log`
30
+ - `Omnitrack::Result` and `Omnitrack::MultiResult` value objects
31
+ - ActiveJob integration for async dispatch
32
+ - Retry logic with exponential back-off
33
+ - Rails view helpers: `omnitrack_tags`, `omnitrack_event_tag`
34
+ - Controller concern: `Omnitrack::Controller` (alias of `Omnitrack::Concerns::Controller`)
35
+ - `rails generate omnitrack:install` generator
36
+ - Rake tasks: `omnitrack:status`, `omnitrack:test_event`, `omnitrack:rotate_log`
37
+ - Adapter-level error isolation (never raises into host app)
38
+ - SHA-256 PII hashing for all platforms
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) OmniTrack contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,364 @@
1
+ # OmniTrack-rb 🎯
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/omnitrack-rb)](https://rubygems.org/gems/omnitrack-rb)
4
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-red)](https://www.ruby-lang.org)
5
+ [![Rails](https://img.shields.io/badge/rails-%3E%3D%206.0-red)](https://rubyonrails.org)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.txt)
7
+
8
+ **Production-grade, modular tracking and conversion system for Ruby on Rails.**
9
+
10
+ OmniTrack dispatches events to multiple ad/analytics platforms through a clean adapter pattern — works identically in full-stack and API-only Rails apps, never crashes your main application, and emits structured JSON logs to a dedicated file.
11
+
12
+ **Practical usage (file placement + method examples):** [USAGE.md](USAGE.md)
13
+ **Add the gem to any Rails app without boot errors (ENV, jobs, checklists):** [AI_GEM_SETUP.md](AI_GEM_SETUP.md)
14
+ **ENV variable names (copy into host `.env.example`):** [.env.example](.env.example)
15
+
16
+ ---
17
+
18
+ ## Supported Platforms
19
+
20
+ | Platform | Adapter | Server-side API | JS Pixel |
21
+ |---|---|---|---|
22
+ | Google Ads | `:google_ads` | ✅ Click Conversion Upload | via `omnitrack_tags` |
23
+ | Google Analytics 4 | `:google_analytics` | ✅ Measurement Protocol | via `omnitrack_tags` |
24
+ | Meta (Facebook/Instagram) | `:meta` | ✅ Conversions API | via `omnitrack_tags` |
25
+ | TikTok Ads | `:tiktok` | ✅ Events API | via `omnitrack_tags` |
26
+ | Snapchat Ads | `:snapchat` | ✅ Conversions API | via `omnitrack_tags` |
27
+ | Custom | Your class | ✅ Extend `Base` | — |
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ Add to your `Gemfile`:
34
+
35
+ ```ruby
36
+ gem "omnitrack-rb"
37
+ ```
38
+
39
+ Run the installer:
40
+
41
+ ```bash
42
+ bundle install
43
+ rails generate omnitrack:install
44
+ ```
45
+
46
+ This creates `config/initializers/omnitrack.rb` with a fully commented template.
47
+
48
+ ---
49
+
50
+ ## Configuration
51
+
52
+ ```ruby
53
+ # config/initializers/omnitrack.rb
54
+
55
+ Omnitrack.configure do |config|
56
+ # :auto — detects api_only? at runtime (recommended)
57
+ # :frontend — inject JS tags + use cookies
58
+ # :backend — server-side only (Conversions APIs)
59
+ # :hybrid — both simultaneously
60
+ config.mode = :auto
61
+
62
+ config.adapters = {
63
+ google_ads: {
64
+ enabled: ENV["GOOGLE_ADS_ENABLED"] == "true",
65
+ customer_id: ENV["GOOGLE_ADS_CUSTOMER_ID"],
66
+ developer_token: ENV["GOOGLE_ADS_DEVELOPER_TOKEN"],
67
+ access_token: ENV["GOOGLE_ADS_ACCESS_TOKEN"],
68
+ conversion_action_id: ENV["GOOGLE_ADS_CONVERSION_ACTION_ID"]
69
+ },
70
+ google_analytics: {
71
+ enabled: ENV["GA4_ENABLED"] == "true",
72
+ measurement_id: ENV["GA4_MEASUREMENT_ID"],
73
+ api_secret: ENV["GA4_API_SECRET"]
74
+ },
75
+ meta: {
76
+ enabled: ENV["META_ENABLED"] == "true",
77
+ pixel_id: ENV["META_PIXEL_ID"],
78
+ access_token: ENV["META_ACCESS_TOKEN"]
79
+ },
80
+ tiktok: {
81
+ enabled: ENV["TIKTOK_ENABLED"] == "true",
82
+ pixel_id: ENV["TIKTOK_PIXEL_ID"],
83
+ access_token: ENV["TIKTOK_ACCESS_TOKEN"]
84
+ },
85
+ snapchat: {
86
+ enabled: ENV["SNAPCHAT_ENABLED"] == "true",
87
+ pixel_id: ENV["SNAPCHAT_PIXEL_ID"],
88
+ access_token: ENV["SNAPCHAT_ACCESS_TOKEN"]
89
+ }
90
+ }
91
+
92
+ config.auto_capture = true # capture gclid, fbclid, ttclid, UTMs automatically
93
+ config.log_level = :info
94
+ config.async = Rails.env.production? # use ActiveJob in production
95
+ config.timeout = 5
96
+ config.max_retries = 3
97
+
98
+ # Optional: error hook for Sentry / Honeybadger
99
+ config.on_error = ->(error, adapter) {
100
+ Sentry.capture_exception(error, tags: { adapter: adapter.name })
101
+ }
102
+ end
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Usage
108
+
109
+ ### Full-stack Rails (views + controllers)
110
+
111
+ **Layout** — inject all platform pixels into `<head>`:
112
+
113
+ ```erb
114
+ <!DOCTYPE html>
115
+ <html>
116
+ <head>
117
+ <%= omnitrack_tags %>
118
+ </head>
119
+ ```
120
+
121
+ **Controller** — track events server-side (`Omnitrack::Controller` is auto-included via the Railtie; you can also `include Omnitrack::Controller` explicitly in `ApplicationController` if needed):
122
+
123
+ ```ruby
124
+ class OrdersController < ApplicationController
125
+ def create
126
+ @order = Order.create!(order_params)
127
+
128
+ # Fires through all enabled server-side adapters
129
+ omnitrack_event("purchase",
130
+ value: @order.total,
131
+ currency: "USD",
132
+ order_id: @order.id)
133
+
134
+ redirect_to @order
135
+ end
136
+ end
137
+ ```
138
+
139
+ **View** — emit a JS event tag on the confirmation page:
140
+
141
+ ```erb
142
+ <%= omnitrack_event_tag("purchase", value: @order.total, currency: "USD") %>
143
+ ```
144
+
145
+ ### API-only Rails
146
+
147
+ No JS involved — everything is server-side:
148
+
149
+ ```ruby
150
+ class Api::V1::OrdersController < ApplicationController
151
+ def create
152
+ @order = Order.create!(order_params)
153
+
154
+ Omnitrack.track("purchase",
155
+ value: @order.total,
156
+ currency: "USD",
157
+ order_id: @order.id,
158
+ # Pass click IDs from your mobile client via request body or headers:
159
+ fbclid: params[:fbclid],
160
+ gclid: params[:gclid])
161
+
162
+ render json: @order
163
+ end
164
+ end
165
+ ```
166
+
167
+ ### Service objects / background jobs
168
+
169
+ ```ruby
170
+ class PurchaseTracker
171
+ def call(order)
172
+ Omnitrack.track("purchase",
173
+ value: order.total,
174
+ currency: order.currency,
175
+ order_id: order.id,
176
+ email: order.user.email)
177
+ end
178
+ end
179
+ ```
180
+
181
+ ### Identify a user
182
+
183
+ ```ruby
184
+ Omnitrack.identify(
185
+ email: current_user.email,
186
+ phone: current_user.phone,
187
+ first_name: current_user.first_name,
188
+ last_name: current_user.last_name,
189
+ external_id: current_user.id.to_s
190
+ )
191
+ ```
192
+
193
+ > PII (email, phone, name) is SHA-256 hashed before being sent to any platform.
194
+
195
+ ---
196
+
197
+ ## Working with Results
198
+
199
+ Every call returns an `Omnitrack::MultiResult`:
200
+
201
+ ```ruby
202
+ result = Omnitrack.track("purchase", value: 99.0)
203
+
204
+ result.success? # => true if ALL adapters succeeded
205
+ result.any_failure? # => true if at least one adapter failed
206
+ result.failures # => [Omnitrack::Result, ...]
207
+ result.successes # => [Omnitrack::Result, ...]
208
+
209
+ result.each do |r|
210
+ puts "#{r.adapter}: #{r.status}" # => google_ads: success
211
+ end
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Building a Custom Adapter
217
+
218
+ ```ruby
219
+ # app/tracking/my_platform_adapter.rb
220
+
221
+ class MyPlatformAdapter < Omnitrack::Adapters::Base
222
+ self.adapter_name = :my_platform
223
+
224
+ def track_event(event_name, payload = {})
225
+ safe_execute(event_name) do
226
+ response = http_post(
227
+ "https://api.myplatform.com/events",
228
+ body: {
229
+ event: event_name,
230
+ data: payload,
231
+ api_key: config[:api_key]
232
+ }
233
+ )
234
+ Omnitrack::Result.success(adapter: name, data: JSON.parse(response.body))
235
+ end
236
+ end
237
+
238
+ def track_conversion(data = {})
239
+ track_event("conversion", data)
240
+ end
241
+
242
+ def identify_user(user_data = {})
243
+ # store or send user data
244
+ Omnitrack::Result.success(adapter: name)
245
+ end
246
+
247
+ private
248
+
249
+ def validate_config
250
+ require_config!(:api_key, hint: "MyPlatform API key")
251
+ end
252
+ end
253
+ ```
254
+
255
+ Register in config:
256
+
257
+ ```ruby
258
+ Omnitrack.configure do |config|
259
+ config.adapters[:my_platform] = {
260
+ enabled: true,
261
+ api_key: ENV["MY_PLATFORM_API_KEY"]
262
+ }
263
+ end
264
+
265
+ # Register the class
266
+ Omnitrack::Registry.register(MyPlatformAdapter)
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Logging
272
+
273
+ OmniTrack writes structured JSON to `log/omnitrack.log` (separate from Rails' main log):
274
+
275
+ ```json
276
+ {"timestamp":"2024-01-15T10:23:45.123Z","message":"adapter.request","adapter":"meta","event":"purchase","payload":{"value":99.0}}
277
+ {"timestamp":"2024-01-15T10:23:45.891Z","message":"adapter.response","adapter":"meta","event":"purchase","status":"success","duration_ms":768.4}
278
+ {"timestamp":"2024-01-15T10:23:46.002Z","message":"adapter.error","adapter":"google_ads","event":"purchase","error":"Omnitrack::AdapterError","message":"HTTP 401 from googleads.googleapis.com"}
279
+ ```
280
+
281
+ Configure log level:
282
+
283
+ ```ruby
284
+ config.log_level = :debug # :debug | :info | :warn | :error | :none
285
+ ```
286
+
287
+ Rotate logs:
288
+
289
+ ```bash
290
+ rails omnitrack:rotate_log
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Rake Tasks
296
+
297
+ ```bash
298
+ rails omnitrack:status # Show configured adapters and status
299
+ rails omnitrack:test_event # Send a test event through all enabled adapters
300
+ rails omnitrack:test_event[purchase] # Send a named test event
301
+ rails omnitrack:rotate_log # Rotate the log file
302
+ ```
303
+
304
+ ---
305
+
306
+ ## Async / Background Jobs
307
+
308
+ When `config.async = true`, tracking calls are dispatched via `ActiveJob`:
309
+
310
+ ```ruby
311
+ config.async = true
312
+ config.queue_name = :omnitrack
313
+ ```
314
+
315
+ Ensure your queue adapter is configured in production:
316
+
317
+ ```ruby
318
+ # config/environments/production.rb
319
+ config.active_job.queue_adapter = :sidekiq
320
+ ```
321
+
322
+ ---
323
+
324
+ ## Middleware
325
+
326
+ `Omnitrack::Middleware::RequestTracker` is automatically inserted into your Rack stack. It:
327
+
328
+ 1. Captures `gclid`, `fbclid`, `ttclid`, UTMs, IP, and User-Agent from every request
329
+ 2. Makes them available at `Omnitrack::Context.current` throughout the request
330
+ 3. Clears thread-local state after each response (no cross-request leakage)
331
+
332
+ ---
333
+
334
+ ## Thread Safety
335
+
336
+ - All shared state uses `Mutex`-guarded access
337
+ - Context is stored in `Thread.current` — safe under Puma/Sidekiq
338
+ - Adapters are instantiated per-dispatch (stateless between requests)
339
+
340
+ ---
341
+
342
+ ## Development
343
+
344
+ ```bash
345
+ bundle install
346
+ bundle exec rspec
347
+ bundle exec rubocop
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Contributing
353
+
354
+ 1. Fork the repository
355
+ 2. Create your branch (`git checkout -b feature/my-adapter`)
356
+ 3. Write tests
357
+ 4. Run `bundle exec rspec` — all green
358
+ 5. Open a Pull Request
359
+
360
+ ---
361
+
362
+ ## License
363
+
364
+ MIT. See [LICENSE.txt](LICENSE.txt).