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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +11 -10
- data/lib/generators/wide_events/install/install_generator.rb +5 -1
- data/lib/generators/wide_events/install/templates/agents_md_section.md +1 -1
- data/lib/wide_event/test_helper.rb +7 -3
- data/lib/wide_event/version.rb +1 -1
- data/skills/debugging-with-wide-events/SKILL.md +5 -4
- data/skills/instrumenting-wide-events/SKILL.md +12 -11
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 861efab741f9c9c6445d2c5f877e08c6f7da36e01254719191d1976baaff398b
|
|
4
|
+
data.tar.gz: d2f7efdca7e96cd0f4d65589014ab0a137bde8ce22c1b3edd9bc15dd21425606
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
config.
|
|
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.
|
|
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",
|
|
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") {
|
|
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
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
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
|
data/lib/wide_event/version.rb
CHANGED
|
@@ -54,14 +54,15 @@ before reaching for logs or the trace waterfall.
|
|
|
54
54
|
|
|
55
55
|
## Standing queries
|
|
56
56
|
|
|
57
|
-
- **
|
|
58
|
-
|
|
59
|
-
|
|
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+
|
|
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
|
-
|
|
17
|
-
|
|
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: `
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
94
|
+
5. Opaque IDs are declared `pii: opaque_id`; anything questionable is flagged `pii: review`.
|