track_relay 1.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 +7 -0
- data/CHANGELOG.md +147 -0
- data/LICENSE.txt +21 -0
- data/README.md +458 -0
- data/UPGRADING.md +85 -0
- data/USAGE.md +192 -0
- data/lib/generators/track_relay/event/event_generator.rb +17 -0
- data/lib/generators/track_relay/event/templates/event.rb.tt +21 -0
- data/lib/generators/track_relay/install/install_generator.rb +49 -0
- data/lib/generators/track_relay/install/templates/application_subscriber.rb.tt +31 -0
- data/lib/generators/track_relay/install/templates/initializer.rb.tt +42 -0
- data/lib/generators/track_relay/install/templates/sample_catalog.rb.tt +17 -0
- data/lib/generators/track_relay/subscriber/subscriber_generator.rb +17 -0
- data/lib/generators/track_relay/subscriber/templates/subscriber.rb.tt +28 -0
- data/lib/tasks/track_relay.rake +80 -0
- data/lib/track_relay/catalog.rb +86 -0
- data/lib/track_relay/client_id/ahoy_visitor.rb +34 -0
- data/lib/track_relay/client_id/ga.rb +48 -0
- data/lib/track_relay/client_id/session.rb +46 -0
- data/lib/track_relay/configuration.rb +141 -0
- data/lib/track_relay/controller_tracking.rb +90 -0
- data/lib/track_relay/current.rb +33 -0
- data/lib/track_relay/delivery_job.rb +84 -0
- data/lib/track_relay/dispatcher.rb +92 -0
- data/lib/track_relay/dsl/event_builder.rb +64 -0
- data/lib/track_relay/dsl/param_builder.rb +74 -0
- data/lib/track_relay/errors.rb +54 -0
- data/lib/track_relay/event_definition.rb +74 -0
- data/lib/track_relay/event_payload.rb +244 -0
- data/lib/track_relay/instrumenter.rb +241 -0
- data/lib/track_relay/job_tracking.rb +50 -0
- data/lib/track_relay/linter.rb +218 -0
- data/lib/track_relay/manifest.rb +85 -0
- data/lib/track_relay/railtie.rb +97 -0
- data/lib/track_relay/subscribers/ahoy.rb +110 -0
- data/lib/track_relay/subscribers/base.rb +231 -0
- data/lib/track_relay/subscribers/ga4_measurement_protocol.rb +250 -0
- data/lib/track_relay/subscribers/logger.rb +79 -0
- data/lib/track_relay/subscribers/test.rb +60 -0
- data/lib/track_relay/testing/helpers.rb +44 -0
- data/lib/track_relay/testing/minitest_assertions.rb +71 -0
- data/lib/track_relay/testing/rspec_matchers.rb +79 -0
- data/lib/track_relay/testing.rb +90 -0
- data/lib/track_relay/validators/catalog_validator.rb +48 -0
- data/lib/track_relay/validators/ga4_constraints.rb +85 -0
- data/lib/track_relay/version.rb +5 -0
- data/lib/track_relay.rb +203 -0
- metadata +248 -0
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Upgrading track_relay
|
|
2
|
+
|
|
3
|
+
This document summarizes breaking changes between `track_relay` releases
|
|
4
|
+
and how to migrate. For the full release history, see
|
|
5
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
6
|
+
|
|
7
|
+
## 0.1.0 → 0.2.0
|
|
8
|
+
|
|
9
|
+
No breaking changes to the Ruby gem surface. New features added:
|
|
10
|
+
|
|
11
|
+
- `TrackRelay::Subscribers::Ga4MeasurementProtocol` — wire with
|
|
12
|
+
`config.ga4_measurement_id` and `config.ga4_api_secret`.
|
|
13
|
+
- `config.client_id_resolvers` — ordered chain (GA cookie, Ahoy
|
|
14
|
+
visitor, session fallback). Default chain preserves existing
|
|
15
|
+
`_ga` cookie behavior.
|
|
16
|
+
- Subscriber-side `only:` / `except:` filters added to
|
|
17
|
+
`TrackRelay.subscribe(klass, only:, except:)`.
|
|
18
|
+
- New rake task: `track_relay:lint:ga4` — audits your catalog
|
|
19
|
+
against GA4 constraints (snake_case, max 40 chars per event name,
|
|
20
|
+
max 25 custom params per event, reserved-name refusal).
|
|
21
|
+
|
|
22
|
+
No host-app code changes are required. Optional adoption:
|
|
23
|
+
`bundle exec rake track_relay:lint:ga4` and add the GA4 subscriber if
|
|
24
|
+
you use Google Analytics.
|
|
25
|
+
|
|
26
|
+
## 0.2.0 → 0.3.0
|
|
27
|
+
|
|
28
|
+
**One BREAKING change (JavaScript client only). Ruby gem surface is
|
|
29
|
+
unaffected.**
|
|
30
|
+
|
|
31
|
+
### BREAKING: `init({ manifestUrl })` no longer requires `measurementId`
|
|
32
|
+
|
|
33
|
+
In `@track_relay/client`, the `init({ manifestUrl })` call no longer
|
|
34
|
+
requires a `measurementId` parameter. If your code relied on the
|
|
35
|
+
missing-`measurementId` throw to detect misconfiguration, add an
|
|
36
|
+
explicit assertion before calling `init`:
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
import { init } from "@track_relay/client";
|
|
40
|
+
|
|
41
|
+
const measurementId = process.env.GA4_MEASUREMENT_ID;
|
|
42
|
+
if (!measurementId) {
|
|
43
|
+
throw new Error("GA4_MEASUREMENT_ID is required");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
init({ manifestUrl: "/track_relay_catalog.json", measurementId });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### New features in 0.3.0
|
|
50
|
+
|
|
51
|
+
- `TrackRelay::Subscribers::Ahoy` — server-side subscriber that uses
|
|
52
|
+
only the public Ahoy API (`controller.ahoy.track` /
|
|
53
|
+
`current_visit.track`). Wire with
|
|
54
|
+
`config.subscribe TrackRelay::Subscribers::Ahoy.new` (requires the
|
|
55
|
+
`ahoy_matey` gem).
|
|
56
|
+
- `AhoyJs` export added in `@track_relay/client` — wraps
|
|
57
|
+
`window.ahoy.track` using the same event names as the server.
|
|
58
|
+
|
|
59
|
+
## 0.3.0 → 1.0.0
|
|
60
|
+
|
|
61
|
+
**No breaking changes.** 1.0.0 adds:
|
|
62
|
+
|
|
63
|
+
- Three Rails generators: `track_relay:install`, `track_relay:event`,
|
|
64
|
+
`track_relay:subscriber`. Run
|
|
65
|
+
`bin/rails generate track_relay:install` in your existing app —
|
|
66
|
+
the inject step is idempotent and skips if
|
|
67
|
+
`include TrackRelay::ControllerTracking` is already present.
|
|
68
|
+
- Public-API stability guarantee. See [README.md](README.md#public-api-stability)
|
|
69
|
+
for the stable surface; classes outside that list (`EventPayload`,
|
|
70
|
+
`Instrumenter`, `Dispatcher`, `Catalog`, `Current`, `DeliveryJob`,
|
|
71
|
+
`ClientId::*`) are internal and may change without a major bump.
|
|
72
|
+
- E2E test coverage proving the install generator's output works
|
|
73
|
+
end-to-end through a real controller call.
|
|
74
|
+
- Documentation: getting-started guide at [USAGE.md](USAGE.md).
|
|
75
|
+
|
|
76
|
+
To upgrade:
|
|
77
|
+
|
|
78
|
+
1. Bump your Gemfile pin to `gem "track_relay", "~> 1.0"`.
|
|
79
|
+
2. `bundle update track_relay`.
|
|
80
|
+
3. (Optional but recommended) Run
|
|
81
|
+
`bin/rails generate track_relay:install` to refresh your initializer
|
|
82
|
+
with the latest comments and ApplicationSubscriber base class.
|
|
83
|
+
Existing files will trigger a Thor "overwrite?" prompt; the inject
|
|
84
|
+
step is idempotent regardless.
|
|
85
|
+
4. `bundle exec rake test` — should pass without further changes.
|
data/USAGE.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# track_relay — Getting Started
|
|
2
|
+
|
|
3
|
+
A walkthrough of the typical install path, from `bundle install` to your
|
|
4
|
+
first asserted-tracked event.
|
|
5
|
+
|
|
6
|
+
## 1. Install
|
|
7
|
+
|
|
8
|
+
Add to your Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "track_relay", "~> 1.0"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bundle install
|
|
18
|
+
bin/rails generate track_relay:install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The install generator scaffolds:
|
|
22
|
+
- `config/initializers/track_relay.rb` — richly commented; one Logger
|
|
23
|
+
subscriber active by default, plus commented-out scaffolds for Test,
|
|
24
|
+
GA4, and Ahoy.
|
|
25
|
+
- `config/track_relay/sample.rb` — a working `event :hello_world`
|
|
26
|
+
declaration to prove your install works.
|
|
27
|
+
- `app/track_relay/subscribers/application_subscriber.rb` — base class
|
|
28
|
+
for your custom subscribers.
|
|
29
|
+
- `include TrackRelay::ControllerTracking` injected into
|
|
30
|
+
`app/controllers/application_controller.rb` (idempotent — no-ops if
|
|
31
|
+
already present).
|
|
32
|
+
|
|
33
|
+
Verify it works:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bundle exec rake test
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This should pass cleanly.
|
|
40
|
+
|
|
41
|
+
## 2. Define your first event
|
|
42
|
+
|
|
43
|
+
The install generator created `config/track_relay/sample.rb`:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
TrackRelay.catalog do
|
|
47
|
+
event :hello_world do
|
|
48
|
+
string :message, required: true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You can add more events to the same file, or generate one per file:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bin/rails generate track_relay:event ArticleViewed
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
That writes `config/track_relay/article_viewed.rb`:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
TrackRelay.catalog do
|
|
63
|
+
event :article_viewed do
|
|
64
|
+
# integer :id, required: true
|
|
65
|
+
# string :slug, required: true
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Uncomment and edit the typed param stubs as needed. Supported types:
|
|
71
|
+
`integer`, `string`, `float`, `boolean`, `datetime`. Validators:
|
|
72
|
+
`required:`, `max:`, `in:`, `format:`, `sanitize:`.
|
|
73
|
+
|
|
74
|
+
## 3. Track from a controller
|
|
75
|
+
|
|
76
|
+
The install generator already added the `ControllerTracking` concern to
|
|
77
|
+
your `ApplicationController`. Inside any controller action:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
class ArticlesController < ApplicationController
|
|
81
|
+
def show
|
|
82
|
+
track :article_viewed, id: params[:id].to_i, slug: "the-slug"
|
|
83
|
+
# ... render normally
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Tracking is fire-and-forget; subscribers run via ActiveJob unless they
|
|
89
|
+
opt into `synchronous!`.
|
|
90
|
+
|
|
91
|
+
## 4. Add subscribers
|
|
92
|
+
|
|
93
|
+
The default install enables `TrackRelay::Subscribers::Logger` so every
|
|
94
|
+
event hits `Rails.logger`. Edit `config/initializers/track_relay.rb` to
|
|
95
|
+
wire additional destinations.
|
|
96
|
+
|
|
97
|
+
For a custom subscriber:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
bin/rails generate track_relay:subscriber Slack
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
That writes `app/track_relay/subscribers/slack_subscriber.rb`. Edit the
|
|
104
|
+
`#deliver(payload)` body, then register in
|
|
105
|
+
`config/initializers/track_relay.rb`:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
TrackRelay.configure do |config|
|
|
109
|
+
config.subscribe SlackSubscriber.new
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Built-in subscribers:
|
|
114
|
+
- `TrackRelay::Subscribers::Test` — in-memory capture for tests
|
|
115
|
+
- `TrackRelay::Subscribers::Logger` — Rails.logger sink
|
|
116
|
+
- `TrackRelay::Subscribers::Ga4MeasurementProtocol` — GA4 server-side
|
|
117
|
+
(requires `ga4_measurement_id` + `ga4_api_secret` config)
|
|
118
|
+
- `TrackRelay::Subscribers::Ahoy` — ahoy_matey integration (requires
|
|
119
|
+
the `ahoy_matey` gem in your Gemfile)
|
|
120
|
+
|
|
121
|
+
> **Heads up — Ahoy bot exclusion.** ahoy_matey silently drops events
|
|
122
|
+
> from requests that look bot-like (logged as `[ahoy] Event excluded`).
|
|
123
|
+
> If you're testing via `curl`, Postman, or anything with a non-browser
|
|
124
|
+
> User-Agent, no row will land in `ahoy_events`. Use a real browser or
|
|
125
|
+
> pass a Chrome/Firefox UA header. This is Ahoy's behavior, not
|
|
126
|
+
> track_relay's.
|
|
127
|
+
|
|
128
|
+
## 5. Test your events
|
|
129
|
+
|
|
130
|
+
`track_relay` ships Minitest and RSpec test helpers. In Minitest:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
require "track_relay/testing/helpers"
|
|
134
|
+
|
|
135
|
+
class ArticlesControllerTest < ActionDispatch::IntegrationTest
|
|
136
|
+
include TrackRelay::Testing::Helpers
|
|
137
|
+
|
|
138
|
+
test "show tracks article_viewed" do
|
|
139
|
+
get article_path(42)
|
|
140
|
+
assert_tracked :article_viewed, id: 42, slug: "the-slug"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The helpers auto-call `TrackRelay.test_mode!` in setup and
|
|
146
|
+
`test_mode_off!` in teardown.
|
|
147
|
+
|
|
148
|
+
For RSpec, use the `have_tracked(:event).with(params)` matcher exposed
|
|
149
|
+
by `TrackRelay::Testing::RSpec` — see the README for full details.
|
|
150
|
+
|
|
151
|
+
## 6. Untyped events and the linter
|
|
152
|
+
|
|
153
|
+
By default, `untyped_events_allowed = true` — `track :anything` works
|
|
154
|
+
without a catalog entry. To audit untyped events, enable the JSONL log:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
config.untyped_log_path = Rails.root.join("tmp/track_relay_untyped.jsonl")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Then run:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
bundle exec rake track_relay:lint
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
This reports every untyped event seen, with file and line context, so
|
|
167
|
+
you can promote them to typed catalog entries.
|
|
168
|
+
|
|
169
|
+
## 7. GA4 + client-side
|
|
170
|
+
|
|
171
|
+
For GA4 server-side, set the credentials and subscribe:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
config.ga4_measurement_id = ENV.fetch("GA4_MEASUREMENT_ID")
|
|
175
|
+
config.ga4_api_secret = ENV.fetch("GA4_API_SECRET")
|
|
176
|
+
config.subscribe TrackRelay::Subscribers::Ga4MeasurementProtocol.new
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Then run `bundle exec rake track_relay:lint:ga4` to audit your catalog
|
|
180
|
+
against GA4 constraints (snake_case names, max 40 chars, max 25 custom
|
|
181
|
+
params per event, reserved-name refusal).
|
|
182
|
+
|
|
183
|
+
For client-side, install `@track_relay/client` from npm and call
|
|
184
|
+
`init({ manifestUrl: "/track_relay_catalog.json" })` after the manifest
|
|
185
|
+
is generated by `bundle exec rake track_relay:manifest` (or
|
|
186
|
+
automatically by the Railtie's asset-precompile hook).
|
|
187
|
+
|
|
188
|
+
## Next
|
|
189
|
+
|
|
190
|
+
- [README.md](README.md) — full feature reference
|
|
191
|
+
- [UPGRADING.md](UPGRADING.md) — migration notes if upgrading from 0.x
|
|
192
|
+
- [CHANGELOG.md](CHANGELOG.md) — release history
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module TrackRelay
|
|
6
|
+
module Generators
|
|
7
|
+
class EventGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Creates a typed catalog entry stub at config/track_relay/<name>.rb."
|
|
11
|
+
|
|
12
|
+
def create_event_file
|
|
13
|
+
template "event.rb.tt", "config/track_relay/#{file_name}.rb"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# <%= class_name %> event — defined here so the track_relay Railtie
|
|
4
|
+
# autoloads it from config/track_relay/ at boot.
|
|
5
|
+
#
|
|
6
|
+
# Track this event from a controller or job:
|
|
7
|
+
# track :<%= file_name %>, # required + optional params here
|
|
8
|
+
#
|
|
9
|
+
# Run `bundle exec rake track_relay:lint:ga4` to audit GA4 constraints.
|
|
10
|
+
|
|
11
|
+
TrackRelay.catalog do
|
|
12
|
+
event :<%= file_name %> do
|
|
13
|
+
# Uncomment and edit:
|
|
14
|
+
# integer :id, required: true
|
|
15
|
+
# string :label, required: true
|
|
16
|
+
# string :category
|
|
17
|
+
# float :value
|
|
18
|
+
# boolean :active
|
|
19
|
+
# datetime :occurred_at
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module TrackRelay
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Creates a TrackRelay initializer, sample catalog, and ApplicationSubscriber, and includes TrackRelay::ControllerTracking in ApplicationController."
|
|
11
|
+
|
|
12
|
+
def create_initializer
|
|
13
|
+
template "initializer.rb.tt", "config/initializers/track_relay.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_sample_catalog
|
|
17
|
+
template "sample_catalog.rb.tt", "config/track_relay/sample.rb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_application_subscriber
|
|
21
|
+
template "application_subscriber.rb.tt", "app/track_relay/subscribers/application_subscriber.rb"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def inject_controller_tracking
|
|
25
|
+
controller_path = "app/controllers/application_controller.rb"
|
|
26
|
+
unless File.exist?(File.join(destination_root, controller_path))
|
|
27
|
+
say_status :skip, "#{controller_path} not found; add `include TrackRelay::ControllerTracking` manually", :yellow
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
existing = File.read(File.join(destination_root, controller_path))
|
|
32
|
+
if existing.include?("TrackRelay::ControllerTracking")
|
|
33
|
+
say_status :identical, "#{controller_path} already includes TrackRelay::ControllerTracking", :blue
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
inject_into_class controller_path, "ApplicationController", " include TrackRelay::ControllerTracking\n"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def post_install_message
|
|
41
|
+
say ""
|
|
42
|
+
say "TrackRelay installed.", :green
|
|
43
|
+
say " Edit config/initializers/track_relay.rb to wire subscribers."
|
|
44
|
+
say " Edit config/track_relay/sample.rb (or rails g track_relay:event NAME) to define events."
|
|
45
|
+
say " Run `bundle exec rake test` — it should pass cleanly out of the box."
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ApplicationSubscriber — base class for your custom track_relay subscribers.
|
|
4
|
+
#
|
|
5
|
+
# Subclass this to add your own destinations:
|
|
6
|
+
#
|
|
7
|
+
# class MyAnalyticsSubscriber < ApplicationSubscriber
|
|
8
|
+
# def deliver(payload)
|
|
9
|
+
# # send payload.name + payload.params somewhere
|
|
10
|
+
# end
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# Register in config/initializers/track_relay.rb:
|
|
14
|
+
#
|
|
15
|
+
# TrackRelay.configure do |config|
|
|
16
|
+
# config.subscribe MyAnalyticsSubscriber.new
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Run `rails g track_relay:subscriber NAME` to scaffold a subscriber.
|
|
20
|
+
class ApplicationSubscriber < TrackRelay::Subscribers::Base
|
|
21
|
+
# Uncomment to run inline instead of via DeliveryJob:
|
|
22
|
+
# synchronous!
|
|
23
|
+
|
|
24
|
+
# payload.name => :event_name (Symbol)
|
|
25
|
+
# payload.params => { key: value, ... } (typed and coerced)
|
|
26
|
+
# payload.context => { controller:, action:, client_id:, user:, ... }
|
|
27
|
+
# payload.timestamp => Time
|
|
28
|
+
def deliver(payload)
|
|
29
|
+
raise NotImplementedError, "#{self.class.name} must implement #deliver(payload)"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# TrackRelay configuration
|
|
4
|
+
# See https://github.com/dchuk/track_relay#readme for full documentation.
|
|
5
|
+
# Run `bundle exec rake track_relay:lint` to audit untyped events.
|
|
6
|
+
|
|
7
|
+
TrackRelay.configure do |config|
|
|
8
|
+
# ----- Subscribers --------------------------------------------------------
|
|
9
|
+
# Logger subscriber: writes every event to Rails.logger and (optionally)
|
|
10
|
+
# captures untyped events to a JSONL file for audit.
|
|
11
|
+
config.subscribe TrackRelay::Subscribers::Logger.new
|
|
12
|
+
|
|
13
|
+
# Test subscriber: in-memory capture for use with `assert_tracked`.
|
|
14
|
+
# Prefer `TrackRelay.test_mode!` in test setup over registering this here.
|
|
15
|
+
# config.subscribe TrackRelay::Subscribers::Test.new if Rails.env.test?
|
|
16
|
+
|
|
17
|
+
# ----- GA4 Measurement Protocol (server-side) ----------------------------
|
|
18
|
+
# config.ga4_measurement_id = ENV.fetch("GA4_MEASUREMENT_ID", nil)
|
|
19
|
+
# config.ga4_api_secret = ENV.fetch("GA4_API_SECRET", nil)
|
|
20
|
+
# config.ga4_use_eu_endpoint = false
|
|
21
|
+
# config.subscribe TrackRelay::Subscribers::Ga4MeasurementProtocol.new
|
|
22
|
+
|
|
23
|
+
# ----- Ahoy (server-side) ------------------------------------------------
|
|
24
|
+
# Requires the `ahoy_matey` gem in your Gemfile.
|
|
25
|
+
# config.subscribe TrackRelay::Subscribers::Ahoy.new
|
|
26
|
+
|
|
27
|
+
# ----- Untyped events ----------------------------------------------------
|
|
28
|
+
# Allow tracking calls for events not yet declared in the catalog.
|
|
29
|
+
# When false, untyped events raise in dev/test and log in production.
|
|
30
|
+
# config.untyped_events_allowed = true
|
|
31
|
+
|
|
32
|
+
# Path for the untyped-events audit log (used by `rake track_relay:lint`).
|
|
33
|
+
# config.untyped_log_path = Rails.root.join("tmp/track_relay_untyped.jsonl")
|
|
34
|
+
|
|
35
|
+
# ----- Validation behavior -----------------------------------------------
|
|
36
|
+
# In production, subscriber errors are swallowed and logged (default true).
|
|
37
|
+
# In development/test, they are re-raised after fan-out (default false).
|
|
38
|
+
# config.swallow_subscriber_errors = false
|
|
39
|
+
|
|
40
|
+
# Raise on schema validation errors (default true in dev/test, false in prod).
|
|
41
|
+
# config.raise_on_validation_error = true
|
|
42
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Sample catalog — define your events here.
|
|
4
|
+
#
|
|
5
|
+
# The Railtie autoloads every *.rb file under config/track_relay/ at boot
|
|
6
|
+
# and reloads them on every code reload in development.
|
|
7
|
+
#
|
|
8
|
+
# Run `rails g track_relay:event NAME` to scaffold a new event in its own file.
|
|
9
|
+
|
|
10
|
+
TrackRelay.catalog do
|
|
11
|
+
# Tutorial event: prove your install works.
|
|
12
|
+
# In a controller action: `track :hello_world, message: "hi"`
|
|
13
|
+
# In a test: `assert_tracked :hello_world, message: "hi"`
|
|
14
|
+
event :hello_world do
|
|
15
|
+
string :message, required: true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module TrackRelay
|
|
6
|
+
module Generators
|
|
7
|
+
class SubscriberGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Creates a subscriber class stub at app/track_relay/subscribers/<name>_subscriber.rb."
|
|
11
|
+
|
|
12
|
+
def create_subscriber_file
|
|
13
|
+
template "subscriber.rb.tt", "app/track_relay/subscribers/#{file_name}_subscriber.rb"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# <%= class_name %>Subscriber — custom track_relay subscriber.
|
|
4
|
+
#
|
|
5
|
+
# Register in config/initializers/track_relay.rb:
|
|
6
|
+
#
|
|
7
|
+
# TrackRelay.configure do |config|
|
|
8
|
+
# config.subscribe <%= class_name %>Subscriber.new
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# If you ran `rails g track_relay:install`, you can subclass
|
|
12
|
+
# ApplicationSubscriber instead of TrackRelay::Subscribers::Base
|
|
13
|
+
# to share behavior across your subscribers.
|
|
14
|
+
class <%= class_name %>Subscriber < TrackRelay::Subscribers::Base
|
|
15
|
+
# Uncomment to run inline instead of via DeliveryJob:
|
|
16
|
+
# synchronous!
|
|
17
|
+
|
|
18
|
+
# Filter to specific events only:
|
|
19
|
+
# filter only: %i[<%= file_name %>]
|
|
20
|
+
|
|
21
|
+
# payload.name => :event_name (Symbol)
|
|
22
|
+
# payload.params => { key: value, ... } (typed and coerced)
|
|
23
|
+
# payload.context => { controller:, action:, client_id:, user:, ... }
|
|
24
|
+
# payload.timestamp => Time
|
|
25
|
+
def deliver(payload)
|
|
26
|
+
# TODO: send payload to your destination
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Audit untyped (non-catalog) events captured in the JSONL sink written
|
|
4
|
+
# by {TrackRelay::Subscribers::Logger}.
|
|
5
|
+
#
|
|
6
|
+
# Both tasks ABORT WITH NONZERO exit when
|
|
7
|
+
# `TrackRelay.config.untyped_log_path` is unset. From 01-CONTEXT.md /
|
|
8
|
+
# the plan's must_haves: a silent exit-0 on a misconfigured audit task
|
|
9
|
+
# is a footgun (the user thinks the audit "passed" when in fact nothing
|
|
10
|
+
# was ever recorded). When the path IS set, the task exits 0 — lint is
|
|
11
|
+
# a report, not a gate.
|
|
12
|
+
|
|
13
|
+
# Loaded via require_relative (not the gem's umbrella `require
|
|
14
|
+
# "track_relay"`) so this rake file stays file-disjoint with Plan 02-02
|
|
15
|
+
# — `lib/track_relay.rb` is owned by that plan in the same wave.
|
|
16
|
+
require_relative "../track_relay/manifest"
|
|
17
|
+
|
|
18
|
+
namespace :track_relay do
|
|
19
|
+
desc "Audit untyped events captured in the JSONL sink (config.untyped_log_path)"
|
|
20
|
+
task lint: :environment do
|
|
21
|
+
path = TrackRelay.config.untyped_log_path
|
|
22
|
+
if path.nil?
|
|
23
|
+
abort <<~MSG
|
|
24
|
+
track_relay: untyped_log_path is not configured.
|
|
25
|
+
Set it in config/initializers/track_relay.rb:
|
|
26
|
+
|
|
27
|
+
TrackRelay.configure do |c|
|
|
28
|
+
c.untyped_log_path = Rails.root.join("tmp/track_relay_untyped.jsonl")
|
|
29
|
+
c.subscribe TrackRelay::Subscribers::Logger.new
|
|
30
|
+
end
|
|
31
|
+
MSG
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
TrackRelay::Linter.new(path).print
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "Audit untyped events and emit JSON report to stdout"
|
|
38
|
+
task "lint:json" => :environment do
|
|
39
|
+
path = TrackRelay.config.untyped_log_path
|
|
40
|
+
abort "track_relay: untyped_log_path is not configured" if path.nil?
|
|
41
|
+
|
|
42
|
+
puts TrackRelay::Linter.new(path).to_json
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "Audit untyped events for GA4 event-name constraint violations"
|
|
46
|
+
task "lint:ga4" => :environment do
|
|
47
|
+
path = TrackRelay.config.untyped_log_path
|
|
48
|
+
if path.nil?
|
|
49
|
+
abort <<~MSG
|
|
50
|
+
track_relay: untyped_log_path is not configured.
|
|
51
|
+
Set it in config/initializers/track_relay.rb:
|
|
52
|
+
|
|
53
|
+
TrackRelay.configure do |c|
|
|
54
|
+
c.untyped_log_path = Rails.root.join("tmp/track_relay_untyped.jsonl")
|
|
55
|
+
c.subscribe TrackRelay::Subscribers::Logger.new
|
|
56
|
+
end
|
|
57
|
+
MSG
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
clean = TrackRelay::Linter.new(path).print_ga4
|
|
61
|
+
# Exit non-zero when violations exist, zero when clean. Lets CI
|
|
62
|
+
# pipelines gate on `rake track_relay:lint:ga4` without parsing
|
|
63
|
+
# output.
|
|
64
|
+
exit(clean ? 0 : 1)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "Generate public/track_relay_catalog.json from the loaded catalog"
|
|
68
|
+
task manifest: :environment do
|
|
69
|
+
# Footgun guard (RISK-04): an empty manifest tells the JS client
|
|
70
|
+
# "no schema, accept everything" — silently. Abort loudly so the
|
|
71
|
+
# operator notices the catalog never loaded.
|
|
72
|
+
if TrackRelay::Catalog.all.empty?
|
|
73
|
+
abort "[track_relay] aborting: catalog is empty (no events registered — check config/track_relay/**/*.rb)"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
path = TrackRelay::Manifest.write!
|
|
77
|
+
count = TrackRelay::Catalog.all.size
|
|
78
|
+
puts "[track_relay] manifest written to #{path} (#{count} #{(count == 1) ? "event" : "events"})"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "track_relay/errors"
|
|
4
|
+
|
|
5
|
+
module TrackRelay
|
|
6
|
+
# Process-wide registry of event definitions and user properties.
|
|
7
|
+
#
|
|
8
|
+
# The {DSL::EventBuilder} pushes definitions in via {register}; the
|
|
9
|
+
# rest of the gem (and host applications) read them out via {lookup},
|
|
10
|
+
# {defined?}, and {all}.
|
|
11
|
+
#
|
|
12
|
+
# State is module-level by design — the gem assumes one catalog per
|
|
13
|
+
# Ruby process, populated during boot. Tests that need isolation call
|
|
14
|
+
# {clear!} in `setup` / `teardown` to reset between cases.
|
|
15
|
+
#
|
|
16
|
+
# @example Registering and looking up an event
|
|
17
|
+
# TrackRelay.catalog do
|
|
18
|
+
# event :article_viewed do
|
|
19
|
+
# integer :article_id, required: true
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# TrackRelay::Catalog.lookup(:article_viewed)
|
|
24
|
+
# # => #<TrackRelay::EventDefinition name=:article_viewed ...>
|
|
25
|
+
module Catalog
|
|
26
|
+
@definitions = {}
|
|
27
|
+
@user_properties = {}
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# @return [Hash{Symbol => Symbol}] catalog-wide user properties
|
|
31
|
+
attr_reader :user_properties
|
|
32
|
+
|
|
33
|
+
# Register a new {EventDefinition} in the catalog.
|
|
34
|
+
#
|
|
35
|
+
# @param definition [EventDefinition]
|
|
36
|
+
# @raise [TrackRelay::CatalogError] when an event with the same
|
|
37
|
+
# name is already registered (defensive guard against catalog
|
|
38
|
+
# bugs that could silently shadow events)
|
|
39
|
+
# @return [EventDefinition]
|
|
40
|
+
def register(definition)
|
|
41
|
+
if @definitions.key?(definition.name)
|
|
42
|
+
raise CatalogError,
|
|
43
|
+
"Event #{definition.name.inspect} is already registered. Call TrackRelay::Catalog.clear! before re-registering (e.g. in tests)."
|
|
44
|
+
end
|
|
45
|
+
@definitions[definition.name] = definition
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Register a catalog-wide user property.
|
|
49
|
+
#
|
|
50
|
+
# @param name [Symbol]
|
|
51
|
+
# @param type [Symbol]
|
|
52
|
+
# @return [Symbol] the type, for chaining
|
|
53
|
+
def register_user_property(name, type)
|
|
54
|
+
@user_properties[name] = type
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param name [Symbol]
|
|
58
|
+
# @return [EventDefinition, nil]
|
|
59
|
+
def lookup(name)
|
|
60
|
+
@definitions[name]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param name [Symbol]
|
|
64
|
+
# @return [Boolean] whether an event with the given name is
|
|
65
|
+
# registered
|
|
66
|
+
def defined?(name)
|
|
67
|
+
@definitions.key?(name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Array<EventDefinition>] frozen array of all registered
|
|
71
|
+
# definitions
|
|
72
|
+
def all
|
|
73
|
+
@definitions.values.freeze
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Reset the registry. Intended for test isolation; do not call in
|
|
77
|
+
# production code.
|
|
78
|
+
#
|
|
79
|
+
# @return [void]
|
|
80
|
+
def clear!
|
|
81
|
+
@definitions = {}
|
|
82
|
+
@user_properties = {}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|