wide_events 0.1.2 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c53fd3a4d33b817af708d0675bfa0b8047e2487a354d62cfb9e3c915b9c3472
4
- data.tar.gz: 747298c60ca63d3a90db33abbf69d430e20a67d5037f79dcbf6d018ab5c86cb4
3
+ metadata.gz: 861efab741f9c9c6445d2c5f877e08c6f7da36e01254719191d1976baaff398b
4
+ data.tar.gz: d2f7efdca7e96cd0f4d65589014ab0a137bde8ce22c1b3edd9bc15dd21425606
5
5
  SHA512:
6
- metadata.gz: d54b65bd07d6294109efb49020299da91aeb040000c42841a175f1b7af3e7fb5a6989971fee3fae5a57f8c0d2c5692572cd08bd4fb978c5798cb7ce9ce98783f
7
- data.tar.gz: e2a3a0d2d6400b1c1cd951c3f6c71c059ecc0cd2b67342beba43901a8ca30929b98ecafe5cab47994e0c3a5153b9773f186670a73ef7eb13894fff40bd9055f3
6
+ metadata.gz: 49017796cabf1aea3026d39049e8ffdd085b9806e81574797cecc14e2e2c46502db6e7a0f8a37c44c26cc78f86f4d71d29f5db2576108970bee290c76dad39ba
7
+ data.tar.gz: 14201839ae140bb2f4a35bef52c581a95b9e6a0939db7a16b12feea7a8ac8cdf46f449564720829df689b63eaf0c198d268448498fac70f5955073aed4bc84e0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.1.3 (2026-08-03)
4
+
5
+ - README links to the docs are absolute GitHub URLs, so they work on
6
+ rubygems.org (relative links 404 there).
7
+ - The install generator adds the registry-enforcement teardown to
8
+ `test/test_helper.rb`, and `capture_wide_events` temporarily enables
9
+ instrumentation while restoring the previous enabled state and sink.
10
+ - Documentation and bundled agent skills clarify privacy handling, unnamed
11
+ exception queries, request/job timing, and one-generation-per-event examples.
12
+
3
13
  ## v0.1.2 (2026-08-03)
4
14
 
5
15
  - README "In production" section and `docs/production-example.md`: an
data/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
 
6
6
  One wide telemetry event per Rails request or job execution, in a database you own.
7
7
 
8
- 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.
8
+ 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, and every question becomes one query against storage you run. ClickHouse with HyperDX is a proven pairing (both open source); any OTLP backend works. If you'd rather not run tracing, the log sink emits one JSON event per request.
9
9
 
10
- 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.
10
+ That format matters more now that agents build with you. Telemetry stops being something humans only glance at and becomes something software queries repeatedly: an agent can instrument a feature, deploy it, and verify it in production. One row per request is a dense way to feed production behavior back into a context window, and owning the storage avoids metered per-event and per-query observability fees.
11
11
 
12
12
  ## Install
13
13
 
@@ -86,7 +86,7 @@ With the endpoint set, wide events enable themselves: the railtie inserts the mi
86
86
  - `job.class`, `job.queue`, `job.queue_latency_ms`, `job.executions`, `job.scheduled` (Solid Queue recurring detection built in, detector pluggable)
87
87
  - `error`, `exception.type`, `exception.message`, `uptime_sec`, `main: true`
88
88
 
89
- A request and the jobs it enqueues are separate units of work: the request's wide event records the enqueue inside its timings, and each job execution emits its own event carrying `job.queue_latency_ms` (time spent waiting in the queue), so queue pressure is visible per job, not smeared into request latency.
89
+ A request and the jobs it enqueues are separate units of work. Synchronous enqueue work contributes to the request's total duration; each job execution emits its own event carrying `job.queue_latency_ms` (time spent waiting in the queue), so queue pressure is visible per job rather than folded into request latency.
90
90
 
91
91
  ## Adding your own attributes
92
92
 
@@ -95,7 +95,7 @@ WideEvent.set("report.id" => report.id, "report.format" => "pdf")
95
95
  WideEvent.phase("pdf_render") { render_pdf } # -> pdf_render.duration_ms
96
96
  ```
97
97
 
98
- 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.
98
+ Attribute writes are safe no-ops outside a unit of work; `phase` still executes and returns the wrapped block. Telemetry failures never raise into app code, so a telemetry bug cannot fail a request or job.
99
99
 
100
100
  Errors come in two shapes, and the difference is the point:
101
101
 
@@ -107,7 +107,7 @@ rescue Vendor::ApiError => e
107
107
  # Unhandled: any exception that escapes gets error: true with NO slug, automatically
108
108
  ```
109
109
 
110
- That makes `error = true AND exception.slug IS NULL` a standing query: every row is a failure nobody wrote a rescue for, which is a permanent, prioritized to-do list of rescues worth instrumenting.
110
+ Filtering for `error = true`, a missing `exception.slug`, and a present `exception.type` isolates exception paths that escaped without a named handled failure. Treat the results as a prioritized investigation queue; add a rescue and `error!` only when handling the exception is intentional.
111
111
 
112
112
  ## The registry is a schema, not a wiki page
113
113
 
@@ -195,8 +195,9 @@ WideEvent.configure do |config|
195
195
  "OpenTelemetry::Instrumentation::PG" => "postgres_query",
196
196
  "OpenTelemetry::Instrumentation::Net::HTTP" => "http_call"
197
197
  }
198
- config.scheduled_job_detector = ->(job) { ... } # default detects Solid Queue recurring executions
199
- config.error_handler = ->(exception, message) { ... } # default reports via OpenTelemetry.handle_error
198
+ # Optional overrides; defaults detect Solid Queue recurring executions and report via OpenTelemetry.handle_error:
199
+ # config.scheduled_job_detector = ->(job) { MyApp.scheduled_job?(job) }
200
+ # config.error_handler = ->(exception, message) { Rails.error.report(exception, context: { message: message }) }
200
201
  end
201
202
  ```
202
203
 
@@ -204,7 +205,7 @@ Log sink notes: events go to `config.logger` (or `Rails.logger`) at info level a
204
205
 
205
206
  ## Querying: ClickHouse + HyperDX
206
207
 
207
- [docs/clickhouse-hyperdx.md](docs/clickhouse-hyperdx.md) has a local quickstart (one container) and a query cookbook. The flavor:
208
+ [docs/clickhouse-hyperdx.md](https://github.com/adammiribyan/wide_events/blob/main/docs/clickhouse-hyperdx.md) has a local quickstart (one container) and a query cookbook. The flavor:
208
209
 
209
210
  ```sql
210
211
  SELECT SpanAttributes['http.route.controller'] AS controller,
@@ -218,11 +219,11 @@ GROUP BY controller ORDER BY p50_ms DESC LIMIT 15
218
219
 
219
220
  ## In production
220
221
 
221
- Wide Events was extracted from a multi-tenant Rails app subject to healthcare privacy rules, where it runs in staging and production today: one row per request and job execution, exported over OTLP into self-hosted ClickStack. A request that runs a hybrid search and drafts a reply with an LLM still lands as one event, carrying the account, the feature flags evaluated, search quality (semantic hits kept, top cosine similarity), the model, tokens, latency, and cost, the Postgres query count, and an error slug for anything rescued along the way. [docs/production-example.md](docs/production-example.md) shows a full anonymized event, the instrumentation patterns behind it, and two standing queries taken from that deployment.
222
+ Wide Events was extracted from a multi-tenant Rails app subject to healthcare privacy rules, where it runs in staging and production today: one row per request and job execution, exported over OTLP into self-hosted ClickStack. A request that runs a hybrid search and drafts a reply with an LLM still lands as one event, carrying the account, the feature flags evaluated, search quality (semantic hits kept, top cosine similarity), the model, tokens, latency, and cost, the Postgres query count, and an error slug for anything rescued along the way. [docs/production-example.md](https://github.com/adammiribyan/wide_events/blob/main/docs/production-example.md) shows a full anonymized event, the instrumentation patterns behind it, and two standing queries taken from that deployment.
222
223
 
223
224
  ## Conventions
224
225
 
225
- 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.
226
+ Flat keys, dot namespaces, snake_case leaves. Durations end in `_duration_ms`, counts in `_count`, booleans read as assertions, timestamps serialize to RFC 3339. Apply your deployment's privacy policy to every identifier: opaque IDs are preferable to names or emails but should still be registered as `pii: opaque_id`. Exclude request params and free text, or flag them as `pii: review` for explicit handling.
226
227
 
227
228
  ## Development
228
229
 
@@ -28,13 +28,17 @@ module WideEvents
28
28
  if content.match?(%r{require "rails/test_help"\n}) && content.match?(/class TestCase\n/)
29
29
  inject_into_file "test/test_helper.rb", "require \"wide_event/test_helper\"\n",
30
30
  after: %r{require "rails/test_help"\n}
31
- inject_into_file "test/test_helper.rb", " include WideEvent::TestHelper\n",
31
+ inject_into_file "test/test_helper.rb", <<~RUBY.indent(4),
32
+ include WideEvent::TestHelper
33
+ teardown { assert_registered_wide_event_attributes }
34
+ RUBY
32
35
  after: /class TestCase\n/
33
36
  else
34
37
  say_status :skip, "test/test_helper.rb (not found or unrecognized) - wire the test helper manually:", :yellow
35
38
  say <<~MSG
36
39
  require "wide_event/test_helper" # after rails/test_help
37
40
  include WideEvent::TestHelper # inside ActiveSupport::TestCase
41
+ teardown { assert_registered_wide_event_attributes }
38
42
  MSG
39
43
  end
40
44
  end
@@ -5,7 +5,7 @@ root span, marked `main = true`). When you add or change a feature,
5
5
  instrument it:
6
6
 
7
7
  - `WideEvent.set("report.id" => report.id)` for domain attributes
8
- - `WideEvent.phase("pdf_render") { ... }` to time a block into `pdf_render.duration_ms`
8
+ - `WideEvent.phase("pdf_render") { render_pdf }` to time a block into `pdf_render.duration_ms`
9
9
  - `WideEvent.error!(slug: "err-report-source-missing", exception: e, expected: true)` in rescue blocks
10
10
 
11
11
  Naming: dot namespaces with snake_case leaves, durations end in
@@ -29,17 +29,21 @@ module WideEvent
29
29
  attrs
30
30
  end
31
31
 
32
- # Swaps the sink for an in-memory one for the duration of the block and
33
- # returns every wide event flushed inside it. Use around code that runs
34
- # its own unit of work (jobs, full request dispatch).
32
+ # Enables instrumentation and swaps the sink for an in-memory one for the
33
+ # duration of the block, restoring both settings afterward. Returns every
34
+ # wide event flushed inside it. Use around code that runs its own unit of
35
+ # work (jobs, full request dispatch).
35
36
  def capture_wide_events(&block)
36
37
  original = WideEvent.config.sink
38
+ original_enabled = WideEvent.config.enabled
37
39
  sink = Sinks::Memory.new
38
40
  WideEvent.config.sink = sink
41
+ WideEvent.config.enabled = true
39
42
  block.call
40
43
  sink.events
41
44
  ensure
42
45
  WideEvent.config.sink = original
46
+ WideEvent.config.enabled = original_enabled
43
47
  end
44
48
 
45
49
  # Fails if strict mode saw any attribute that isn't declared in the
@@ -1,3 +1,3 @@
1
1
  module WideEvent
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
@@ -54,14 +54,15 @@ before reaching for logs or the trace waterfall.
54
54
 
55
55
  ## Standing queries
56
56
 
57
- - **Uninstrumented failures**: `error = 'true' AND SpanAttributes['exception.slug'] = ''`:
58
- failures nobody wrote a rescue for. Each row is a candidate `error!` call;
59
- fixing them is normal instrumentation work (see the
57
+ - **Unnamed exception paths**: `SpanAttributes['error'] = 'true' AND
58
+ SpanAttributes['exception.slug'] = '' AND SpanAttributes['exception.type'] != ''`:
59
+ exceptions that escaped without a named handled failure. Investigate each
60
+ path; add a rescue and `error!` only when handling it is intentional (see the
60
61
  instrumenting-wide-events skill).
61
62
  - **Expected-error drift**: count by `exception.slug` week over week; a
62
63
  slug trending up is a quiet regression.
63
64
  - **Query-count outliers**: max `stats.postgres_query_count` per route; the
64
- top entries are N+1s with names and account ids attached.
65
+ top entries are N+1 candidates with route names and account IDs attached.
65
66
  - **Cold starts**: correlate latency with `uptime_sec_log10` to separate
66
67
  deploy warm-up from real regressions.
67
68
 
@@ -13,8 +13,8 @@ knows: the domain nouns.
13
13
 
14
14
  ## The API
15
15
 
16
- All calls are safe no-ops outside a request/job and never raise into app
17
- code: instrument freely.
16
+ Attribute writes are safe no-ops outside a request/job; `phase` still executes
17
+ and returns its block. Telemetry failures never raise into app code.
18
18
 
19
19
  ```ruby
20
20
  # Flat attributes, merged into the current event
@@ -40,14 +40,15 @@ handle.
40
40
  - Durations end in `_duration_ms` (float milliseconds). Counts end in `_count`.
41
41
  - Booleans read as assertions: `report.cached`, not `report.cache_status`.
42
42
  - Timestamps are RFC 3339 strings (pass a Time; the gem serializes it).
43
- - Sizes and counts over raw content: `note_chars: 2140`, never the note text.
43
+ - Sizes and counts over raw content: `note.body_char_count: 2140`, never the note text.
44
44
 
45
45
  ## PII policy
46
46
 
47
- Opaque ids (user id, account id) are fine. Names, emails, free-text user
48
- input, and request params are not. If an attribute could quote user input,
49
- mark it `pii: review` in the registry and expect it to be scrubbed or
50
- excluded from forwarding.
47
+ Apply the deployment's privacy policy to every identifier. Opaque user and
48
+ account IDs are preferable to names or emails, but still declare them as
49
+ `pii: opaque_id`. Exclude free-text user input and request params. If an
50
+ attribute could quote user input, mark it `pii: review` in the registry and
51
+ expect it to be scrubbed or excluded from forwarding.
51
52
 
52
53
  ## The registry is the contract
53
54
 
@@ -66,8 +67,8 @@ Keys containing `*` are globs for dynamic families (`feature_flag.*`). With
66
67
  `config.strict = true` in the test environment, the suite records undeclared
67
68
  attributes; `assert_registered_wide_event_attributes` fails on them. Run
68
69
  `rake wide_events:registry:docs` if this app generates the human-readable
69
- registry doc, and never rename a shipped attribute without a reason -
70
- renames orphan historical data.
70
+ registry doc, and never rename a shipped attribute without a reason—renames
71
+ orphan historical data.
71
72
 
72
73
  ## Prove it in tests
73
74
 
@@ -82,7 +83,7 @@ end
82
83
  ```
83
84
 
84
85
  For code that runs its own unit of work (jobs, full request dispatch), use
85
- `capture_wide_events { ... }` and assert on the returned events.
86
+ `capture_wide_events { perform_work }` and assert on the returned events.
86
87
 
87
88
  ## Checklist for any instrumentation change
88
89
 
@@ -90,4 +91,4 @@ For code that runs its own unit of work (jobs, full request dispatch), use
90
91
  2. New rescue blocks that swallow errors call `error!` with a unique static slug.
91
92
  3. Every new attribute has a registry entry in the same change.
92
93
  4. A test asserts the attribute is set (`assert_wide_event`).
93
- 5. No PII beyond opaque ids; anything questionable is flagged `pii: review`.
94
+ 5. Opaque IDs are declared `pii: opaque_id`; anything questionable is flagged `pii: review`.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wide_events
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Miribyan