forge_ops_tracker 0.18.0 → 0.19.1

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: e9434056bed332dad65c4a8dc051bace54e7acae535043138833f60ba2586cb0
4
- data.tar.gz: 270cec5a96267fc7d1a41be7c02ab1bfdb9271fb2d7a5e8ae91581a0181848d0
3
+ metadata.gz: a07d319688ef560a28c82ff70a822a2a171d1fe9d4c47973d51093e528e548e2
4
+ data.tar.gz: 6272b6ee64197d04a056a7d34ac4fe5de069be87dea1992f32d30700164ab95a
5
5
  SHA512:
6
- metadata.gz: 34730c4a7e8af87adb9bdaf490d72d923a0bb96b958a9131c28671d3198a43638dc5f13a2331c92b3d1dd506d24b08b6118d6d0559e4c64d5b12b231a0a8c97c
7
- data.tar.gz: 0a9704a43c0c74397e082ab191deab1895de0d17210469b5906c123c33cfd7fe9130992ee10932106a093c4d6c329dadca6ca59da16f46173a063395e0eb7b9c
6
+ metadata.gz: dfe88a94c2008f6ee5af0f47581322647a90253d4d25a2a768e6bfb6bfd759766c15c81315ffb4b030dfd593f893a924ec8c6b7b4be71ad77ff825086c1fc998
7
+ data.tar.gz: 6f087c6828f7a8d45d895d9e26749337dd6683247021187b89281065c5d10e838eb9de985a3fc15aaac2b9a023eceefbc3e7a50878738f13e318bba55390e77e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.1 (2026-10-22)
4
+
5
+ - Strict privacy now also turns off slow query plans (`explain_slow_queries`): no EXPLAIN is run and no
6
+ plan is sent, since a plan carries the SQL. Before, a plan could still be sent, with its values masked.
7
+ - The README says what strict privacy does in its opening lines.
8
+ - The source is public at https://github.com/Luke-Popwell/forge-ops-tracker-ruby, and the package links to it.
9
+
10
+ ## 0.19.0 (2026-10-20)
11
+
12
+ - Strict privacy: a raw request path now keeps only its first segment, and the rest is sent as
13
+ `:path` (`/patients/jane-doe/12` becomes `/patients/:path`; a first segment with a digit or over
14
+ 40 characters becomes `:id`). Before, only segments holding a digit were replaced, so a path like
15
+ `/patients/janedoe` went out whole. A route template (`/patients/:id/notes`, `{id}`, `<int:id>`,
16
+ `[slug]`, `*path`) is kept as it is. This applies everywhere strict privacy names a request or a
17
+ call by its path: span names and transaction names.
18
+
3
19
  ## 0.18.0 (2026-10-20)
4
20
 
5
21
  - New `config.strict_privacy` (off by default, or `FORGE_OPS_STRICT_PRIVACY=true`) for apps that
data/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  Exception reporting client for [ForgeOps](https://getforgeops.net), for Rails, plain Rack and
4
4
  Sinatra apps, and scripts.
5
5
 
6
+ > For health or personal data, turn on [strict privacy](#strict-privacy): the SDK then sends only structured facts about each error (its type, stack trace, release and route), never the message, a raw path, breadcrumbs, or context you haven't allowed.
7
+
6
8
  ## Installation
7
9
 
8
10
  ```ruby
@@ -243,8 +245,10 @@ name, an email or a diagnosis:
243
245
  - Context and tags only keep the top-level keys you list in `privacy_allowed_fields`, and only
244
246
  when the value is a string, number or boolean. Nothing else from them is sent.
245
247
  - Failed jobs and outbound calls keep their error class, never their error message.
246
- - A `transaction_name` that is a raw path rather than a route or `Controller#action` has its ids
247
- taken out (`GET /patients/:id`), in error events, performance samples and failure events alike.
248
+ - A `transaction_name` that is a raw path rather than a route or `Controller#action` keeps only
249
+ its first segment (`GET /patients/jane-doe/12` is sent as `GET /patients/:path`; a first
250
+ segment with a digit becomes `:id`), in error events, performance samples, failure events and
251
+ spans alike. A route template such as `/patients/:id/notes` is kept as it is.
248
252
  - Outbound HTTP spans are named by method and host only (`GET api.example.com`), and span data
249
253
  keeps only the masked `db.statement` and `db.system`.
250
254
 
@@ -31,7 +31,8 @@ module ForgeOpsTracker
31
31
  # without colliding into the same bucket.
32
32
  def record(transaction_name:, duration_ms:, kind: "controller")
33
33
  ensure_worker_started
34
- # Under strict privacy a raw-path name loses its ids; a route or Controller#action is kept.
34
+ # Under strict privacy a raw-path name keeps only its first segment; a route or
35
+ # Controller#action is kept.
35
36
  transaction_name = StrictPrivacy.transaction_name(transaction_name) if configuration.strict_privacy
36
37
 
37
38
  @mutex.synchronize do
@@ -98,7 +98,8 @@ module ForgeOpsTracker
98
98
  # checks first, so a fast query costs one comparison. Returns true only when an EXPLAIN was
99
99
  # queued; never raises.
100
100
  def consider(payload, duration_ms:)
101
- return false unless configuration.explain_slow_queries
101
+ # Strict privacy sends no SQL at all, masked or not (sdks/STRICT_PRIVACY.md).
102
+ return false unless configuration.explain_slow_queries && !configuration.strict_privacy
102
103
  return false unless duration_ms && duration_ms >= configuration.explain_threshold_ms.to_f
103
104
 
104
105
  connection = payload[:connection]
@@ -26,6 +26,7 @@ module ForgeOpsTracker
26
26
  SPAN_DATA_KEYS = %w[db.statement db.system].freeze
27
27
  HTTP_METHOD = /\A(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\S+)/i
28
28
  SCHEME = %r{\A[a-z][a-z0-9+.-]*://}i
29
+ ROUTE_PARAMETER = /\A(?::.+|\{.+\}|<.+>|\[.+\]|\*.*)\z/
29
30
 
30
31
  # Error event fields that are never sent, and the per-frame source context.
31
32
  EVENT_FIELDS_DROPPED = %i[message breadcrumbs endpoint sql_statement].freeze
@@ -83,23 +84,24 @@ module ForgeOpsTracker
83
84
 
84
85
  # "GET https://api.example.com/patients/42?x=1" or "GET api.example.com/patients/42" becomes
85
86
  # "GET api.example.com". A bare path ("/patients/42"), or a method and a path
86
- # ("PUT /orders/17/items"), has its ids taken out, like a page path. A name that's neither
87
+ # ("PUT /orders/17/items"), goes through the path rule, like a page path. A name that's neither
87
88
  # ("OrdersController#create", "patients#show") is kept.
88
89
  def span_name(name)
89
- return path_without_ids(name) if name.to_s.start_with?("/")
90
+ return strict_path(name) if name.to_s.start_with?("/")
90
91
 
91
92
  match = name.to_s.match(HTTP_METHOD)
92
93
  return name unless match
93
94
 
94
95
  method = match[1].upcase
95
96
  target = match[2]
96
- return "#{method} #{path_without_ids(target)}" if target.start_with?("/")
97
+ return "#{method} #{strict_path(target)}" if target.start_with?("/")
97
98
 
98
- "#{method} #{host(target) || path_without_ids(target)}"
99
+ "#{method} #{host(target) || strict_path(target)}"
99
100
  end
100
101
 
101
102
  # Which request something happened in: a route or a controller action is kept, but a raw path
102
- # (what a request no route matched falls back to) has its ids stripped, by the span name rule.
103
+ # (what a request no route matched falls back to) keeps only its first segment, by the span name
104
+ # rule.
103
105
  def transaction_name(name)
104
106
  name.nil? || name.to_s.empty? ? name : span_name(name)
105
107
  end
@@ -119,14 +121,28 @@ module ForgeOpsTracker
119
121
  host if scheme || port || host.include?(".") || host.casecmp?("localhost")
120
122
  end
121
123
 
122
- # A path with every segment that holds a digit, or is long enough to be a token or a name
123
- # slug, replaced by ":id"; no query or fragment. "/patients/42/notes?q=x" is "/patients/:id/notes".
124
- def path_without_ids(path)
125
- segments = path.to_s.split(/[?#]/).first.to_s.split("/").map do |segment|
126
- segment.match?(/\d/) || segment.length > 40 ? ":id" : segment
127
- end
128
- joined = segments.join("/")
129
- joined.empty? ? "/" : joined
124
+ # A request or page path under strict privacy, with no query or fragment. A route template
125
+ # ("/patients/:id/notes", "/patients/{id}", "users/<int:id>/", "/files/*path", "/posts/[slug]")
126
+ # is kept as it is: its words are a developer's, and its parameters are already placeholders. A
127
+ # raw path keeps only its first segment, and the rest becomes ":path": "/patients/jane-doe/12"
128
+ # is "/patients/:path". Anything after the first segment of a raw path could name a person, and
129
+ # no rule can tell a name from a word ("/patients/janedoe"). A first segment that holds a digit
130
+ # or is over 40 characters is ":id" itself.
131
+ def strict_path(path)
132
+ path = path.to_s.split(/[?#]/, 2).first.to_s
133
+ return path if route_template?(path)
134
+
135
+ leading = path.start_with?("/") ? "/" : ""
136
+ segments = path.split("/").reject(&:empty?)
137
+ return "/" if segments.empty?
138
+
139
+ first = segments.first.match?(/[0-9]/) || segments.first.length > 40 ? ":id" : segments.first
140
+ "#{leading}#{first}#{"/:path" if segments.size > 1}"
141
+ end
142
+
143
+ # Whether any segment is a route parameter: ":id", "{id}", "<int:id>", "[slug]" or "*path".
144
+ def route_template?(path)
145
+ path.split("/").any? { |segment| segment.match?(ROUTE_PARAMETER) }
130
146
  end
131
147
 
132
148
  # Only top-level keys named in allowed_fields, and only plain values under them; nil (left out
@@ -1,3 +1,3 @@
1
1
  module ForgeOpsTracker
2
- VERSION = "0.18.0"
2
+ VERSION = "0.19.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forge_ops_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ForgeOps
@@ -147,6 +147,8 @@ licenses:
147
147
  metadata:
148
148
  homepage_uri: https://getforgeops.net
149
149
  rubygems_mfa_required: 'true'
150
+ source_code_uri: https://github.com/Luke-Popwell/forge-ops-tracker-ruby
151
+ changelog_uri: https://github.com/Luke-Popwell/forge-ops-tracker-ruby/blob/main/CHANGELOG.md
150
152
  rdoc_options: []
151
153
  require_paths:
152
154
  - lib