wide_events 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: 60fd90a80072a4f375595529b63b5c739751de76d92d7e29ff1d8581ae6b5a07
4
+ data.tar.gz: 6656514b71043fa80493946446f19582d1a32fe63799f4f7a04e9f67b2cd595f
5
+ SHA512:
6
+ metadata.gz: 43721569390e303660bde87aae8317fe320cd3c9722f33ec0a49e6bebc180258c5536e1596181653bc3302b4010fba56153b70c0129c3c5f458e89fbb055d6e2
7
+ data.tar.gz: 587ec94b148be767437f9d0d126c0ebd321ae5f91db31ef4236ff75cad1d39c5572f8e44d3791a410420b48c7304143260d51049e43607afb2d0573d220ab82b
data/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## v0.1.0 (2026-08-03)
4
+
5
+ First release, extracted from a production Rails app.
6
+
7
+ - Wide-event accumulator (`set`, `phase`, `error!`, `count`) with a
8
+ never-raise contract and per-request/per-job isolation.
9
+ - Rack middleware and ActiveJob instrumentation, wired by a railtie.
10
+ - Sinks: OTel root span (default), JSON log lines, in-memory (tests).
11
+ - Span counter: child-span rollups into `stats.*` (configurable scope map).
12
+ - Notification subscribers: `db.duration_ms`, `view.duration_ms`, capped
13
+ `cache.*` hit/miss booleans.
14
+ - Attribute registry (`config/wide_event/registry.yml`): gem defaults,
15
+ glob entries, strict-mode tracking, `rake wide_events:registry:check`
16
+ and `rake wide_events:registry:docs`.
17
+ - Test helpers: `assert_wide_event`, `capture_wide_events`,
18
+ `assert_registered_wide_event_attributes`.
19
+ - Generators: `wide_events:install` (initializer, registry, AGENTS.md
20
+ section) and `wide_events:skills` (agent skills for writing and
21
+ querying instrumentation).
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Miribyan
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # Wide Events
2
+
3
+ One wide telemetry event per Rails request or job execution, in a database you own.
4
+
5
+ Wide Events collects everything your app knows about each unit of work (route, user, account, build SHA, query counts, cache hits, feature flags, phase timings, errors) into one flat, high-cardinality event on the OpenTelemetry root span you already export. Every request becomes one row. Every question becomes one query against storage you run: ClickHouse with HyperDX on top is a proven pairing (both open source), any OTLP backend works, and a log-line sink emits one JSON event per request if you'd rather not run tracing at all.
6
+
7
+ That format matters more now that agents build with you. Telemetry stops being something humans glance at and becomes something software queries in a loop: an agent that instruments a feature, deploys it, and verifies it in production will hit your observability stack fifty times before lunch. One row per request is the densest way to feed production behavior back into a context window, and owning the storage means the loop has no price per iteration.
8
+
9
+ ## Install
10
+
11
+ ```ruby
12
+ # Gemfile
13
+ gem "wide_events"
14
+ ```
15
+
16
+ ```bash
17
+ bin/rails generate wide_events:install # initializer + attribute registry + AGENTS.md section
18
+ bin/rails generate wide_events:skills # agent skills into .claude/skills/
19
+ ```
20
+
21
+ Wide events turn on wherever `OTEL_EXPORTER_OTLP_ENDPOINT` is set. Dev and test pay nothing.
22
+
23
+ ## What you get per event
24
+
25
+ The gem records the generic attributes on every request and job:
26
+
27
+ - `http.request.id`, `http.response.status_code`, `http.route.controller`, request body size, parsed `user_agent.*`
28
+ - `db.duration_ms` and `view.duration_ms` (Rails' own measurements)
29
+ - `stats.postgres_query_count` / `_duration_ms` and `stats.http_call_count` / `_duration_ms`, rolled up from OTel child spans (scope map is configurable)
30
+ - `cache.<prefix>` hit/miss booleans, capped per event
31
+ - `job.class`, `job.queue`, `job.queue_latency_ms`, `job.executions`, `job.scheduled` (Solid Queue recurring detection built in, detector pluggable)
32
+ - `error`, `exception.type`, `exception.message`, `uptime_sec`, `main: true`
33
+
34
+ Your code adds the parts only it knows:
35
+
36
+ ```ruby
37
+ WideEvent.set("report.id" => report.id, "report.format" => "pdf")
38
+ WideEvent.phase("pdf_render") { render_pdf } # -> pdf_render.duration_ms
39
+ WideEvent.error!(slug: "err-export-source-missing", exception: e, expected: true)
40
+ ```
41
+
42
+ Every call is a safe no-op outside a unit of work and never raises into app code. A telemetry bug cannot fail a request or a job.
43
+
44
+ "Something is slow" turns into "the reports route is slow for account 4218 on build f3a91c, and it's running 742 queries": one query in HyperDX for you, one SQL call for your agent.
45
+
46
+ ## The registry is a schema, not a wiki page
47
+
48
+ Every attribute is declared in `config/wide_event/registry.yml`:
49
+
50
+ ```yaml
51
+ report.format:
52
+ type: string
53
+ set_by: ReportsController#create
54
+ pii: none
55
+ notes: pdf or csv
56
+ ```
57
+
58
+ The gem registers its own attributes by default; globs (`feature_flag.*`) cover dynamic families. With `config.strict = true` in the test environment, the suite records every undeclared attribute it sees, and `assert_registered_wide_event_attributes` fails on them. `rake wide_events:registry:check` validates the file; `rake wide_events:registry:docs` generates the human-readable registry doc from it, so documentation can't drift from reality.
59
+
60
+ ## Testing
61
+
62
+ ```ruby
63
+ # test_helper.rb
64
+ require "wide_event/test_helper"
65
+ class ActiveSupport::TestCase
66
+ include WideEvent::TestHelper
67
+ end
68
+ ```
69
+
70
+ ```ruby
71
+ test "report rendering is instrumented" do
72
+ assert_wide_event("pdf_render.duration_ms", "report.format" => "pdf") do
73
+ Report.new(format: "pdf").render
74
+ end
75
+ end
76
+
77
+ test "job emits one wide event" do
78
+ events = capture_wide_events { ExportJob.perform_now }
79
+ assert_equal 1, events.length
80
+ end
81
+ ```
82
+
83
+ ## Built for the agent loop
84
+
85
+ `bin/rails generate wide_events:skills` installs two agent skills:
86
+
87
+ - **instrumenting-wide-events**: the write path. Naming conventions, when to use `set` vs `phase` vs `error!`, the registry workflow, PII rules, test assertions.
88
+ - **debugging-with-wide-events**: the read path. Symptom-to-query workflow against ClickHouse/HyperDX or JSON logs, plus the standing queries worth running: `error = true AND exception.slug IS NULL` is a permanent, queryable to-do list of rescues nobody instrumented.
89
+
90
+ The install generator also appends a wide-events section to `AGENTS.md`, so every future session knows the instrumentation exists and how to extend it.
91
+
92
+ ## Configuration
93
+
94
+ ```ruby
95
+ WideEvent.configure do |config|
96
+ config.enabled = ENV["OTEL_EXPORTER_OTLP_ENDPOINT"].present? # default
97
+ config.sink = :otel # :otel, :log, or any object responding to flush(attrs)
98
+ config.strict = Rails.env.test? # track attributes against the registry
99
+ config.max_cache_attrs = 10
100
+ config.span_scopes = { # OTel instrumentation scope -> stats.* name
101
+ "OpenTelemetry::Instrumentation::PG" => "postgres_query",
102
+ "OpenTelemetry::Instrumentation::Net::HTTP" => "http_call"
103
+ }
104
+ config.scheduled_job_detector = ->(job) { ... } # default detects Solid Queue recurring executions
105
+ config.error_handler = ->(exception, message) { ... } # default reports via OpenTelemetry.handle_error
106
+ end
107
+ ```
108
+
109
+ The railtie inserts the middleware directly below `ActionDispatch::Executor` (the executor clears the per-request store, so placement matters), instruments `ActiveJob::Base`, and wires the span counter and notification subscribers after your initializers run. Without Rails, call `WideEvent.install!` yourself after configuring the OpenTelemetry SDK.
110
+
111
+ ## Conventions
112
+
113
+ Flat keys, dot namespaces, snake_case leaves. Durations end in `_duration_ms`, counts in `_count`, booleans read as assertions, timestamps serialize to RFC 3339. Opaque ids are fine; names, emails, and request params are not, and anything that could quote user input is flagged `pii: review` in the registry.
114
+
115
+ Unhandled exceptions get `error: true` with no slug, deliberately: the missing slug marks the rescue you haven't instrumented yet.
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ bin/setup # bundle install + appraisal gemfiles
121
+ bundle exec rake test # run the suite
122
+ bin/rubocop # lint (rubocop-rails-omakase)
123
+ ```
124
+
125
+ The CI matrix runs the suite across Ruby 3.2 to 4.0 and Rails 7.1 to main via [Appraisal](https://github.com/thoughtbot/appraisal); run a specific combination locally with `BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test`. Releases go out with `bin/release <version>`.
126
+
127
+ ## Requirements
128
+
129
+ Ruby >= 3.2, Rails >= 7.1 (activesupport and rack are the only hard dependencies; opentelemetry-sdk and useragent are optional and detected at runtime).
130
+
131
+ ## License
132
+
133
+ MIT.
@@ -0,0 +1,26 @@
1
+ require "rails/generators/base"
2
+
3
+ module WideEvents
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def create_initializer
9
+ copy_file "initializer.rb", "config/initializers/wide_events.rb"
10
+ end
11
+
12
+ def create_registry
13
+ copy_file "registry.yml", "config/wide_event/registry.yml"
14
+ end
15
+
16
+ def add_agents_section
17
+ section = File.read(File.expand_path("templates/agents_md_section.md", __dir__))
18
+ if File.exist?(File.join(destination_root, "AGENTS.md"))
19
+ append_to_file "AGENTS.md", "\n#{section}"
20
+ else
21
+ create_file "AGENTS.md", section
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ ## Wide events
2
+
3
+ Every HTTP request and job execution emits one flat telemetry event (the OTel
4
+ root span, marked `main = true`). When you add or change a feature,
5
+ instrument it:
6
+
7
+ - `WideEvent.set("report.id" => report.id)` for domain attributes
8
+ - `WideEvent.phase("pdf_render") { ... }` to time a block into `pdf_render.duration_ms`
9
+ - `WideEvent.error!(slug: "err-report-source-missing", exception: e, expected: true)` in rescue blocks
10
+
11
+ Naming: dot namespaces with snake_case leaves, durations end in
12
+ `_duration_ms`, counts in `_count`, booleans read as assertions. Every new
13
+ attribute must be declared in `config/wide_event/registry.yml` in the same
14
+ change; the test suite fails on undeclared attributes. Assert instrumentation
15
+ with `assert_wide_event` from `wide_event/test_helper`. See the
16
+ `instrumenting-wide-events` skill for full conventions and
17
+ `debugging-with-wide-events` for querying production.
@@ -0,0 +1,11 @@
1
+ WideEvent.configure do |config|
2
+ # Enabled wherever OTEL_EXPORTER_OTLP_ENDPOINT is set, flushing onto the
3
+ # OTel root span. Uncomment to emit JSON log lines instead (no tracing
4
+ # required):
5
+ # config.enabled = true
6
+ # config.sink = :log
7
+
8
+ # Track attributes against config/wide_event/registry.yml in tests; pair
9
+ # with assert_registered_wide_event_attributes to enforce the schema.
10
+ config.strict = Rails.env.test?
11
+ end
@@ -0,0 +1,16 @@
1
+ # Attributes your app sets on wide events, one entry per attribute. The gem's
2
+ # own attributes (main, error, http.*, user_agent.*, job.*, stats.*, cache.*,
3
+ # db/view durations, exception.*) are registered by default: declare only
4
+ # what your code adds. Keys containing `*` are globs for dynamic attributes.
5
+ #
6
+ # Fields: type (string/integer/float/boolean, or a list), set_by, pii
7
+ # (none/opaque_id/review), notes, forward (optional list of sinks that may
8
+ # receive the attribute).
9
+ #
10
+ # Example:
11
+ #
12
+ # report.format:
13
+ # type: string
14
+ # set_by: ReportsController#create
15
+ # pii: none
16
+ # notes: pdf or csv
@@ -0,0 +1,20 @@
1
+ require "rails/generators/base"
2
+
3
+ module WideEvents
4
+ module Generators
5
+ # Copies the agent skills shipped with the gem into .claude/skills/ so
6
+ # coding agents pick up the instrumentation and debugging conventions.
7
+ class SkillsGenerator < Rails::Generators::Base
8
+ SKILLS_ROOT = File.expand_path("../../../../skills", __dir__)
9
+
10
+ source_root SKILLS_ROOT
11
+
12
+ def copy_skills
13
+ Dir[File.join(SKILLS_ROOT, "*", "SKILL.md")].sort.each do |path|
14
+ name = File.basename(File.dirname(path))
15
+ copy_file File.join(name, "SKILL.md"), ".claude/skills/#{name}/SKILL.md"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,63 @@
1
+ module WideEvent
2
+ class Configuration
3
+ # Reports through OTel's error handler when it's loaded; silent otherwise.
4
+ DEFAULT_ERROR_HANDLER = lambda do |exception, message|
5
+ if defined?(OpenTelemetry) && OpenTelemetry.respond_to?(:handle_error)
6
+ OpenTelemetry.handle_error(exception: exception, message: "wide_event #{message}")
7
+ end
8
+ end
9
+
10
+ # Recurring (scheduled) Solid Queue executions have a RecurringExecution
11
+ # row pointing at the underlying job. One indexed query per execution.
12
+ SOLID_QUEUE_DETECTOR = lambda do |job|
13
+ return false unless defined?(SolidQueue::RecurringExecution)
14
+ job.provider_job_id.present? && SolidQueue::RecurringExecution.exists?(job_id: job.provider_job_id)
15
+ rescue StandardError
16
+ false
17
+ end
18
+
19
+ attr_accessor :enabled, :max_cache_attrs, :span_scopes, :strict,
20
+ :logger, :error_handler, :scheduled_job_detector
21
+ attr_reader :sink, :registry_path
22
+
23
+ def initialize
24
+ @enabled = !ENV["OTEL_EXPORTER_OTLP_ENDPOINT"].to_s.empty?
25
+ @sink = :otel
26
+ @resolved_sink = nil
27
+ @max_cache_attrs = 10
28
+ @span_scopes = {
29
+ "OpenTelemetry::Instrumentation::PG" => "postgres_query",
30
+ "OpenTelemetry::Instrumentation::Net::HTTP" => "http_call"
31
+ }
32
+ @strict = false
33
+ @registry_path = nil
34
+ @registry = nil
35
+ @logger = nil
36
+ @error_handler = DEFAULT_ERROR_HANDLER
37
+ @scheduled_job_detector = SOLID_QUEUE_DETECTOR
38
+ end
39
+
40
+ def sink=(value)
41
+ @sink = value
42
+ @resolved_sink = nil
43
+ end
44
+
45
+ def resolved_sink
46
+ @resolved_sink ||= case @sink
47
+ when :otel then Sinks::OtelSpan.new
48
+ when :log then Sinks::LogLine.new(logger)
49
+ else @sink
50
+ end
51
+ end
52
+
53
+ def registry_path=(path)
54
+ @registry_path = path
55
+ @registry = nil
56
+ end
57
+
58
+ def registry
59
+ return nil if @registry_path.nil?
60
+ @registry ||= Registry.load(@registry_path)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,55 @@
1
+ require "active_support/concern"
2
+
3
+ module WideEvent
4
+ # One wide event per job execution: the job-shaped unit of work. Included
5
+ # into ActiveJob::Base by the railtie; include it manually in ApplicationJob
6
+ # when not using the railtie.
7
+ #
8
+ # Known semantics: exceptions later swallowed by retry_on/discard_on still
9
+ # mark this execution error=true: the wide event records that THIS attempt
10
+ # failed, which is accurate per-execution; roll up by job.id to judge the
11
+ # job.
12
+ module JobInstrumentation
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ around_perform :wide_event
17
+ end
18
+
19
+ private
20
+
21
+ def wide_event(&block)
22
+ return block.call unless WideEvent.enabled?
23
+
24
+ WideEvent.with do |attrs|
25
+ enqueued = enqueued_at.is_a?(String) ? Time.iso8601(enqueued_at) : enqueued_at
26
+ WideEvent.set({
27
+ "main" => true,
28
+ "job.class" => self.class.name,
29
+ "job.queue" => queue_name,
30
+ "job.id" => job_id,
31
+ "job.executions" => executions,
32
+ "job.enqueued_at" => enqueued,
33
+ "job.queue_latency_ms" => enqueued ? ((Time.now - enqueued) * 1000).round : nil,
34
+ "job.scheduled" => wide_event_scheduled?
35
+ }.merge(WideEvent.uptime_attributes))
36
+ begin
37
+ block.call
38
+ rescue Exception => e
39
+ WideEvent.set("error" => true, "exception.type" => e.class.name,
40
+ "exception.message" => e.message.to_s[0, 500])
41
+ raise
42
+ ensure
43
+ WideEvent.flush(attrs)
44
+ end
45
+ end
46
+ end
47
+
48
+ def wide_event_scheduled?
49
+ detector = WideEvent.config.scheduled_job_detector
50
+ detector ? detector.call(self) == true : false
51
+ rescue StandardError
52
+ false
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,81 @@
1
+ begin
2
+ require "useragent"
3
+ rescue LoadError
4
+ # Optional: without the useragent gem only user_agent.original is set.
5
+ end
6
+
7
+ module WideEvent
8
+ # Opens the wide-event accumulator for each request and flushes it to the
9
+ # configured sink as the response unwinds. With the OTel sink, the flush
10
+ # target is the root span opened by OTel's Rack instrumentation at the top
11
+ # of the stack. Must be inserted BELOW ActionDispatch::Executor: the
12
+ # executor clears IsolatedExecutionState on completion, so above it the
13
+ # store would be gone before the flush. The railtie handles placement.
14
+ class Middleware
15
+ def initialize(app)
16
+ @app = app
17
+ end
18
+
19
+ def call(env)
20
+ return @app.call(env) unless WideEvent.enabled?
21
+
22
+ WideEvent.with do |attrs|
23
+ seed(env)
24
+ begin
25
+ status, headers, body = @app.call(env)
26
+ ex = env["action_dispatch.exception"]
27
+ WideEvent.set(
28
+ "http.response.status_code" => status.to_i,
29
+ "error" => status.to_i >= 500 || attrs["error"] == true || !ex.nil?
30
+ )
31
+ if ex
32
+ # Rendered by ActionDispatch::ShowExceptions before this
33
+ # middleware's rescue ever fires: error attrs WITHOUT a slug,
34
+ # same convention as the unhandled-exception path below.
35
+ WideEvent.set("error" => true, "exception.type" => ex.class.name,
36
+ "exception.message" => ex.message.to_s[0, 500])
37
+ end
38
+ [ status, headers, body ]
39
+ rescue Exception => e
40
+ # Unhandled exception: error attrs WITHOUT a slug: the standing
41
+ # "instrument this rescue" query keys off the missing slug.
42
+ WideEvent.set("error" => true, "exception.type" => e.class.name,
43
+ "exception.message" => e.message.to_s[0, 500])
44
+ raise
45
+ ensure
46
+ # Read at flush time, on every path (success AND unhandled
47
+ # exception): ActionDispatch::RequestId runs deeper in the stack
48
+ # and mutates env in place, so the id doesn't exist at seed time
49
+ # but does by the time any outcome unwinds back to here.
50
+ WideEvent.set("http.request.id" => env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"])
51
+ WideEvent.flush(attrs)
52
+ end
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def seed(env)
59
+ WideEvent.set({
60
+ "main" => true,
61
+ "http.request.body_size" => env["CONTENT_LENGTH"]&.to_i
62
+ }.merge(user_agent_attributes(env["HTTP_USER_AGENT"])).merge(WideEvent.uptime_attributes))
63
+ end
64
+
65
+ def user_agent_attributes(ua)
66
+ return {} if ua.nil? || ua.empty?
67
+ return { "user_agent.original" => ua[0, 300] } unless defined?(UserAgent)
68
+
69
+ parsed = UserAgent.parse(ua)
70
+ {
71
+ "user_agent.original" => ua[0, 300],
72
+ "user_agent.browser" => parsed.browser,
73
+ "user_agent.browser_version" => parsed.version.to_s,
74
+ "user_agent.os" => parsed.os,
75
+ "user_agent.platform" => parsed.platform
76
+ }
77
+ rescue StandardError
78
+ { "user_agent.original" => ua[0, 300] }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,32 @@
1
+ require "rails/railtie"
2
+
3
+ module WideEvent
4
+ class Railtie < Rails::Railtie
5
+ # Below ActionDispatch::Executor on purpose: the executor clears
6
+ # IsolatedExecutionState on completion; above it the store would be gone
7
+ # before the flush. With OTel Rack instrumentation at the top of the
8
+ # stack, the flush still lands inside the root span.
9
+ initializer "wide_event.middleware" do |app|
10
+ app.middleware.insert_after ActionDispatch::Executor, WideEvent::Middleware
11
+ end
12
+
13
+ initializer "wide_event.active_job" do
14
+ ActiveSupport.on_load(:active_job) { include WideEvent::JobInstrumentation }
15
+ end
16
+
17
+ initializer "wide_event.registry" do |app|
18
+ path = app.root.join("config", "wide_event", "registry.yml")
19
+ WideEvent.config.registry_path ||= path.to_s if path.exist?
20
+ end
21
+
22
+ # After the app's own initializers, so the host's OpenTelemetry::SDK
23
+ # configuration (and any WideEvent.configure block) has already run.
24
+ config.after_initialize do
25
+ WideEvent.install! if WideEvent.enabled?
26
+ end
27
+
28
+ rake_tasks do
29
+ load File.expand_path("tasks/registry.rake", __dir__)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,131 @@
1
+ # Attributes the gem sets on every wide event. App attributes belong in the
2
+ # app's own registry file (config/wide_event/registry.yml), which merges over
3
+ # these entries.
4
+ main:
5
+ type: boolean
6
+ set_by: wide_events
7
+ pii: none
8
+ notes: Marks the root span as a wide event. Query surface is `main = true`.
9
+ error:
10
+ type: boolean
11
+ set_by: wide_events
12
+ pii: none
13
+ notes: Status >= 500, an unhandled exception, or an error! call.
14
+ uptime_sec:
15
+ type: integer
16
+ set_by: wide_events
17
+ pii: none
18
+ notes: Process age at the time of the event.
19
+ uptime_sec_log10:
20
+ type: float
21
+ set_by: wide_events
22
+ pii: none
23
+ notes: log10 of uptime_sec, for cold-start histograms.
24
+ http.request.id:
25
+ type: string
26
+ set_by: wide_events
27
+ pii: none
28
+ notes: X-Request-Id / ActionDispatch request id.
29
+ http.request.body_size:
30
+ type: integer
31
+ set_by: wide_events
32
+ pii: none
33
+ http.response.status_code:
34
+ type: integer
35
+ set_by: wide_events
36
+ pii: none
37
+ http.route.controller:
38
+ type: string
39
+ set_by: wide_events
40
+ pii: none
41
+ notes: Controller#action, from process_action.action_controller.
42
+ user_agent.original:
43
+ type: string
44
+ set_by: wide_events
45
+ pii: none
46
+ notes: Capped at 300 chars.
47
+ user_agent.browser:
48
+ type: string
49
+ set_by: wide_events
50
+ pii: none
51
+ user_agent.browser_version:
52
+ type: string
53
+ set_by: wide_events
54
+ pii: none
55
+ user_agent.os:
56
+ type: string
57
+ set_by: wide_events
58
+ pii: none
59
+ user_agent.platform:
60
+ type: string
61
+ set_by: wide_events
62
+ pii: none
63
+ db.duration_ms:
64
+ type: float
65
+ set_by: wide_events
66
+ pii: none
67
+ notes: Rails' own db_runtime measurement.
68
+ view.duration_ms:
69
+ type: float
70
+ set_by: wide_events
71
+ pii: none
72
+ notes: Rails' own view_runtime measurement.
73
+ "cache.*":
74
+ type: boolean
75
+ set_by: wide_events
76
+ pii: none
77
+ notes: Hit/miss per normalized cache key prefix, capped per event.
78
+ "stats.*":
79
+ type: [integer, float]
80
+ set_by: wide_events
81
+ pii: none
82
+ notes: Per-dependency child span counts and total durations.
83
+ job.class:
84
+ type: string
85
+ set_by: wide_events
86
+ pii: none
87
+ job.queue:
88
+ type: string
89
+ set_by: wide_events
90
+ pii: none
91
+ job.id:
92
+ type: string
93
+ set_by: wide_events
94
+ pii: none
95
+ job.executions:
96
+ type: integer
97
+ set_by: wide_events
98
+ pii: none
99
+ notes: Retry count, 1 on the first attempt.
100
+ job.enqueued_at:
101
+ type: string
102
+ set_by: wide_events
103
+ pii: none
104
+ notes: RFC 3339.
105
+ job.queue_latency_ms:
106
+ type: integer
107
+ set_by: wide_events
108
+ pii: none
109
+ job.scheduled:
110
+ type: boolean
111
+ set_by: wide_events
112
+ pii: none
113
+ notes: True for recurring/scheduled executions (Solid Queue detector by default).
114
+ exception.slug:
115
+ type: string
116
+ set_by: app rescue sites via error!
117
+ pii: none
118
+ notes: Static kebab-case identifier. Absent on unhandled exceptions, by design.
119
+ exception.type:
120
+ type: string
121
+ set_by: wide_events
122
+ pii: none
123
+ exception.message:
124
+ type: string
125
+ set_by: wide_events
126
+ pii: review
127
+ notes: Capped at 500 chars. May quote user input (review before forwarding).
128
+ exception.expected:
129
+ type: boolean
130
+ set_by: app rescue sites via error!
131
+ pii: none