errorgap 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a962a442c79c4d80123b06eb6cd8afb1e41cdce3b944101e8d9b6112343f2279
4
- data.tar.gz: 61cd50f45d49df099b0ecd54cf18442c19cc31d837b275a0f7903322fbc57c1f
3
+ metadata.gz: 75cf1e4a725d5e1c7f484a577814d71403fbdc4e6c6cd4bcec3d0eca30f00478
4
+ data.tar.gz: 98d336466f4eceedc7ad6fc03fff038704d10872e30c6b7f5376528e424dd73b
5
5
  SHA512:
6
- metadata.gz: e93b57a2558bde0c050d4c806ecb62c57d87ef41af342a8a3ede7071d12c3f71033d008c84dcfa250e5b5314f60ae93d4bf6b02ea546954a291b65f6f7516906
7
- data.tar.gz: a456b31419bec30d07d25d3b0fd597d638008e6994c51e2e66f92a67df9bf78ad1e9ab27655ef4ee8abd2e83176b3be681796cf399d77b24bebcf24704093365
6
+ metadata.gz: 44e3afd205bce67d34e40e6b71a5c67b43615dc04acc299f7188f0e01d514844e7b836523a0dcae0e95388b7b38875c76f2ffc012d3a83b316450182b5a2fb36
7
+ data.tar.gz: b8b2d440abe90925b5750cf6526b34449d00d913d5bd47a8d74339ce8ba9c5622abe55949b66465ad1f1f0b0972ee26446c258086a98280c94dcdff36b66a229
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-10-03
9
+
10
+ ### Added
11
+
12
+ - **Errors link to their request.** Every APM transaction (Rack middleware,
13
+ `track_transaction`, `track_job`) now has an id, sent as the transaction's
14
+ `id`, and errors reported while it runs carry it as
15
+ `context.transaction_id`. Errorgap shows the error a request actually raised
16
+ on its trace instead of matching by route and time, and links each
17
+ occurrence to its trace. The id is fiber-local, so concurrent requests never
18
+ share one, and is sent even when APM sampling drops the transaction.
19
+ `Errorgap.current_transaction_id` and `Errorgap.with_transaction_id` expose
20
+ it for custom instrumentation.
21
+
8
22
  ## [0.5.0] - 2026-07-20
9
23
 
10
24
  ### Added
data/README.md CHANGED
@@ -90,6 +90,13 @@ end
90
90
  Enable with `config.apm_enabled = true` (and optionally `config.apm_sample_rate`).
91
91
  Call `Errorgap.flush` before process exit to drain async deliveries.
92
92
 
93
+ Every transaction gets an id, and errors reported while it runs — from the
94
+ middleware, `Errorgap.notify`, or inside `track_transaction`/`track_job` —
95
+ carry it as `context.transaction_id`. Errorgap then shows the error a request
96
+ actually raised on its trace, and links each occurrence to its request.
97
+ `Errorgap.current_transaction_id` returns the id in effect (fiber-local), and
98
+ `Errorgap.with_transaction_id { |id| ... }` scopes one around custom work.
99
+
93
100
  ## Rack
94
101
 
95
102
  ```ruby
@@ -7,30 +7,42 @@ module Errorgap
7
7
  end
8
8
 
9
9
  def call(env)
10
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
11
- SpanCollector.start if apm_enabled?
10
+ return call_without_transaction(env) unless apm_enabled?
12
11
 
13
- status, headers, body = @app.call(env)
14
- [status, headers, body]
15
- rescue Exception => exception # rubocop:disable Lint/RescueException
16
- notify_once(env, exception)
17
- raise
18
- ensure
19
- if apm_enabled?
20
- elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0
21
- record_transaction(env, status || 500, elapsed_ms)
12
+ # Errors raised while this request runs carry its transaction id.
13
+ Errorgap.with_transaction_id do |transaction_id|
14
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
15
+ SpanCollector.start
16
+ begin
17
+ status, headers, body = @app.call(env)
18
+ [status, headers, body]
19
+ rescue Exception => exception # rubocop:disable Lint/RescueException
20
+ notify_once(env, exception)
21
+ raise
22
+ ensure
23
+ elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0
24
+ record_transaction(env, status || 500, elapsed_ms, transaction_id)
25
+ end
22
26
  end
23
27
  end
24
28
 
25
29
  private
26
30
 
31
+ def call_without_transaction(env)
32
+ @app.call(env)
33
+ rescue Exception => exception # rubocop:disable Lint/RescueException
34
+ notify_once(env, exception)
35
+ raise
36
+ end
37
+
27
38
  def apm_enabled?
28
39
  Errorgap.configuration.apm_enabled
29
40
  end
30
41
 
31
- def record_transaction(env, status_code, elapsed_ms)
42
+ def record_transaction(env, status_code, elapsed_ms, transaction_id)
32
43
  spans = SpanCollector.flush
33
44
  txn = Transaction.new(
45
+ id: transaction_id,
34
46
  kind: "web",
35
47
  method: env["REQUEST_METHOD"],
36
48
  path: route_pattern(env),
@@ -17,13 +17,14 @@ module Errorgap
17
17
  end
18
18
 
19
19
  Transaction = Struct.new(
20
- :kind, :method, :path, :path_raw, :status_code,
20
+ :id, :kind, :method, :path, :path_raw, :status_code,
21
21
  :duration_ms, :environment, :occurred_at, :spans,
22
22
  :job_class, :queue,
23
23
  keyword_init: true
24
24
  ) do
25
25
  def to_h
26
26
  {
27
+ id: id,
27
28
  kind: kind,
28
29
  method: method,
29
30
  path: path,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Errorgap
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/errorgap.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
4
+
3
5
  require_relative "errorgap/configuration"
4
6
  require_relative "errorgap/breadcrumbs"
5
7
  require_relative "errorgap/notifier"
@@ -43,6 +45,10 @@ module Errorgap
43
45
  end
44
46
 
45
47
  def notify(error, context: {}, environment: {}, session: {}, params: {}, sync: false)
48
+ # The request or job this error was raised in, so errorgap links the two.
49
+ if (transaction_id = current_transaction_id) && !context.key?(:transaction_id) && !context.key?("transaction_id")
50
+ context = context.merge(transaction_id: transaction_id)
51
+ end
46
52
  notifier.notify(
47
53
  error,
48
54
  context: context,
@@ -75,6 +81,26 @@ module Errorgap
75
81
  )
76
82
  end
77
83
 
84
+ TRANSACTION_ID_KEY = :errorgap_transaction_id
85
+
86
+ # The id of the APM transaction running on this fiber, if any. Notices
87
+ # reported while it is set carry it as `context.transaction_id`.
88
+ def current_transaction_id
89
+ Thread.current[TRANSACTION_ID_KEY]
90
+ end
91
+
92
+ # Run the block as one transaction: a new id is current for its duration
93
+ # (fiber-local, so concurrent requests never share one) and the previous
94
+ # one is restored after. Yields the id.
95
+ def with_transaction_id
96
+ previous = Thread.current[TRANSACTION_ID_KEY]
97
+ id = SecureRandom.uuid
98
+ Thread.current[TRANSACTION_ID_KEY] = id
99
+ yield id
100
+ ensure
101
+ Thread.current[TRANSACTION_ID_KEY] = previous
102
+ end
103
+
78
104
  # Deliver a prebuilt APM transaction, honoring apm_enabled and sampling.
79
105
  def notify_transaction(transaction, sync: false)
80
106
  return unless configuration.apm_enabled
@@ -92,39 +118,45 @@ module Errorgap
92
118
  # receives a SpanRecorder for manual DB/HTTP spans; any automatic
93
119
  # `sql.active_record` spans recorded during the block are merged in.
94
120
  def track_transaction(method: nil, path: nil, path_raw: nil, status_code: nil, kind: "web", environment: nil, sync: false)
95
- SpanCollector.start
96
- started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
97
- occurred = Time.now
98
- begin
99
- yield(SpanRecorder.new) if block_given?
100
- ensure
101
- elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0
102
- transaction = Transaction.new(
103
- kind: kind, method: method, path: path, path_raw: path_raw,
104
- status_code: status_code, duration_ms: elapsed_ms.round(2),
105
- environment: environment || configuration.environment,
106
- occurred_at: occurred, spans: SpanCollector.flush
107
- )
108
- notify_transaction(transaction, sync: sync)
121
+ with_transaction_id do |transaction_id|
122
+ SpanCollector.start
123
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
124
+ occurred = Time.now
125
+ begin
126
+ yield(SpanRecorder.new) if block_given?
127
+ ensure
128
+ elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0
129
+ transaction = Transaction.new(
130
+ id: transaction_id,
131
+ kind: kind, method: method, path: path, path_raw: path_raw,
132
+ status_code: status_code, duration_ms: elapsed_ms.round(2),
133
+ environment: environment || configuration.environment,
134
+ occurred_at: occurred, spans: SpanCollector.flush
135
+ )
136
+ notify_transaction(transaction, sync: sync)
137
+ end
109
138
  end
110
139
  end
111
140
 
112
141
  # Time a background job and deliver it as a `job` transaction.
113
142
  def track_job(job_class, queue: "default", environment: nil, sync: false)
114
- SpanCollector.start
115
- started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
116
- occurred = Time.now
117
- begin
118
- yield(SpanRecorder.new) if block_given?
119
- ensure
120
- elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0
121
- transaction = Transaction.new(
122
- kind: "job", job_class: job_class, queue: queue,
123
- duration_ms: elapsed_ms.round(2),
124
- environment: environment || configuration.environment,
125
- occurred_at: occurred, spans: SpanCollector.flush
126
- )
127
- notify_transaction(transaction, sync: sync)
143
+ with_transaction_id do |transaction_id|
144
+ SpanCollector.start
145
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
146
+ occurred = Time.now
147
+ begin
148
+ yield(SpanRecorder.new) if block_given?
149
+ ensure
150
+ elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0
151
+ transaction = Transaction.new(
152
+ id: transaction_id,
153
+ kind: "job", job_class: job_class, queue: queue,
154
+ duration_ms: elapsed_ms.round(2),
155
+ environment: environment || configuration.environment,
156
+ occurred_at: occurred, spans: SpanCollector.flush
157
+ )
158
+ notify_transaction(transaction, sync: sync)
159
+ end
128
160
  end
129
161
  end
130
162
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: errorgap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Errorgap
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-20 00:00:00.000000000 Z
11
+ date: 2026-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake