errorgap 0.5.0 → 0.7.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: '0841cbcd204e3167a443244774a3f05a4b692dc6b706b76db7cd3af7943296b1'
4
+ data.tar.gz: ec4b272a9c9102c7fc9643c6d66042ded9ee1e7ed9457f5bd59ea05f835293f2
5
5
  SHA512:
6
- metadata.gz: e93b57a2558bde0c050d4c806ecb62c57d87ef41af342a8a3ede7071d12c3f71033d008c84dcfa250e5b5314f60ae93d4bf6b02ea546954a291b65f6f7516906
7
- data.tar.gz: a456b31419bec30d07d25d3b0fd597d638008e6994c51e2e66f92a67df9bf78ad1e9ab27655ef4ee8abd2e83176b3be681796cf399d77b24bebcf24704093365
6
+ metadata.gz: '07229bfc0552894549309bb7b0c54363d59d70d09714cf66ccfc0f71d534c78030e55afceb32129e93d55eee0f1fb17a6b81eb2914986db367708fdfcf66c54d'
7
+ data.tar.gz: 96adaebf03ed7d0ef8f5f127ea1ccefafb6e70690235f6029e52b8fb3c7b4bf96d9e2ff263d41941b168a3b95cc9a8c87805ddffe43a2dfd4178328d05b15bd3
data/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ 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.7.0] - 2026-10-03
9
+
10
+ ### Added
11
+
12
+ - **Releases.** `config.release` (default `ENV["ERRORGAP_RELEASE"]`) is sent
13
+ as `context.release` on every notice, tying errors to the deploy that
14
+ shipped them.
15
+ - **Browser trace links.** The Rack middleware reads the `x-errorgap-trace`
16
+ header sent by `@errorgap/browser` 0.3+ and records it as the transaction's
17
+ `trace_id`, so errorgap links a browser API call to the server request that
18
+ answered it. Only well-formed UUIDs are accepted.
19
+
20
+ ## [0.6.0] - 2026-10-03
21
+
22
+ ### Added
23
+
24
+ - **Errors link to their request.** Every APM transaction (Rack middleware,
25
+ `track_transaction`, `track_job`) now has an id, sent as the transaction's
26
+ `id`, and errors reported while it runs carry it as
27
+ `context.transaction_id`. Errorgap shows the error a request actually raised
28
+ on its trace instead of matching by route and time, and links each
29
+ occurrence to its trace. The id is fiber-local, so concurrent requests never
30
+ share one, and is sent even when APM sampling drops the transaction.
31
+ `Errorgap.current_transaction_id` and `Errorgap.with_transaction_id` expose
32
+ it for custom instrumentation.
33
+
8
34
  ## [0.5.0] - 2026-07-20
9
35
 
10
36
  ### Added
data/README.md CHANGED
@@ -31,9 +31,18 @@ Errorgap.configure do |config|
31
31
  # Skip reporting entirely (errors and APM) in these environments.
32
32
  # Also configurable via ERRORGAP_IGNORE_ENVIRONMENTS="test,development".
33
33
  config.ignore_environments = %w[test development]
34
+
35
+ # The deployed version or commit (defaults to ERRORGAP_RELEASE). Errors carry
36
+ # it, so errorgap ties each one to the deploy that shipped it.
37
+ config.release = ENV["GIT_SHA"]
34
38
  end
35
39
  ```
36
40
 
41
+ When the errorgap browser SDK is on the page, its API calls send an
42
+ `x-errorgap-trace` header; the Rack middleware records it on the request's
43
+ transaction, so the browser's Performance view links each call to the server
44
+ request that answered it.
45
+
37
46
  ## Manual Notification
38
47
 
39
48
  ```ruby
@@ -90,6 +99,13 @@ end
90
99
  Enable with `config.apm_enabled = true` (and optionally `config.apm_sample_rate`).
91
100
  Call `Errorgap.flush` before process exit to drain async deliveries.
92
101
 
102
+ Every transaction gets an id, and errors reported while it runs — from the
103
+ middleware, `Errorgap.notify`, or inside `track_transaction`/`track_job` —
104
+ carry it as `context.transaction_id`. Errorgap then shows the error a request
105
+ actually raised on its trace, and links each occurrence to its request.
106
+ `Errorgap.current_transaction_id` returns the id in effect (fiber-local), and
107
+ `Errorgap.with_transaction_id { |id| ... }` scopes one around custom work.
108
+
93
109
  ## Rack
94
110
 
95
111
  ```ruby
@@ -16,7 +16,8 @@ module Errorgap
16
16
  :apm_sample_rate,
17
17
  :logs_enabled,
18
18
  :minimum_log_level,
19
- :max_breadcrumbs
19
+ :max_breadcrumbs,
20
+ :release
20
21
 
21
22
  def initialize
22
23
  @endpoint = ENV.fetch("ERRORGAP_ENDPOINT", "http://127.0.0.1:3030")
@@ -33,6 +34,9 @@ module Errorgap
33
34
  @logs_enabled = true
34
35
  @minimum_log_level = "info"
35
36
  @max_breadcrumbs = 25
37
+ # The deployed version or commit: errors are attributed to the deploy
38
+ # that shipped them.
39
+ @release = ENV["ERRORGAP_RELEASE"].to_s.strip.empty? ? nil : ENV["ERRORGAP_RELEASE"].strip
36
40
  end
37
41
 
38
42
  def validate!
@@ -61,6 +61,7 @@ module Errorgap
61
61
  environment: @configuration.environment,
62
62
  root_directory: @configuration.root_directory
63
63
  }
64
+ context[:release] = @configuration.release if @configuration.release
64
65
  causes = collect_causes
65
66
  context[:causes] = causes unless causes.empty?
66
67
  context[:breadcrumbs] = @breadcrumbs unless @breadcrumbs.empty?
@@ -7,30 +7,52 @@ 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]
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
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def call_without_transaction(env)
32
+ @app.call(env)
15
33
  rescue Exception => exception # rubocop:disable Lint/RescueException
16
34
  notify_once(env, exception)
17
35
  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)
22
- end
23
36
  end
24
37
 
25
- private
38
+ UUID = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
39
+
40
+ # The x-errorgap-trace header a browser SDK sent with this call, linking
41
+ # the browser's view of it to this transaction. Only a well-formed UUID.
42
+ def browser_trace_id(env)
43
+ value = env["HTTP_X_ERRORGAP_TRACE"].to_s.strip
44
+ value.match?(UUID) ? value.downcase : nil
45
+ end
26
46
 
27
47
  def apm_enabled?
28
48
  Errorgap.configuration.apm_enabled
29
49
  end
30
50
 
31
- def record_transaction(env, status_code, elapsed_ms)
51
+ def record_transaction(env, status_code, elapsed_ms, transaction_id)
32
52
  spans = SpanCollector.flush
33
53
  txn = Transaction.new(
54
+ id: transaction_id,
55
+ trace_id: browser_trace_id(env),
34
56
  kind: "web",
35
57
  method: env["REQUEST_METHOD"],
36
58
  path: route_pattern(env),
@@ -17,13 +17,15 @@ module Errorgap
17
17
  end
18
18
 
19
19
  Transaction = Struct.new(
20
- :kind, :method, :path, :path_raw, :status_code,
20
+ :id, :trace_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,
28
+ trace_id: trace_id,
27
29
  kind: kind,
28
30
  method: method,
29
31
  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.7.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.7.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-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake