forge_ops_tracker 0.18.0 → 0.19.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: e9434056bed332dad65c4a8dc051bace54e7acae535043138833f60ba2586cb0
4
- data.tar.gz: 270cec5a96267fc7d1a41be7c02ab1bfdb9271fb2d7a5e8ae91581a0181848d0
3
+ metadata.gz: 3d585c1acbde6c8bb06ec05505e21bf4d784974ed8e655c9398fef0c7e595d03
4
+ data.tar.gz: af67478c90a33dd594bbccf712e48dc817d5e99ae6b61edf0a0fca62e816e4ec
5
5
  SHA512:
6
- metadata.gz: 34730c4a7e8af87adb9bdaf490d72d923a0bb96b958a9131c28671d3198a43638dc5f13a2331c92b3d1dd506d24b08b6118d6d0559e4c64d5b12b231a0a8c97c
7
- data.tar.gz: 0a9704a43c0c74397e082ab191deab1895de0d17210469b5906c123c33cfd7fe9130992ee10932106a093c4d6c329dadca6ca59da16f46173a063395e0eb7b9c
6
+ metadata.gz: ae806f1ba9cb14102494df6f6efca7685ce4900c281fda85c72fe9ad595f5ce3b03b7daf74762ef69de002e0c229d3155027eee4bd3c8d876646b824f4ee1ab4
7
+ data.tar.gz: b774e7abc22663d23f728c71ee7d358180994ccc31b23eb334d393c39da7d403561456b4b484bbe34f3f1678c7f619a40ac94c977a4086ccac66f852790cdb85
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.0 (2026-10-20)
4
+
5
+ - Strict privacy: a raw request path now keeps only its first segment, and the rest is sent as
6
+ `:path` (`/patients/jane-doe/12` becomes `/patients/:path`; a first segment with a digit or over
7
+ 40 characters becomes `:id`). Before, only segments holding a digit were replaced, so a path like
8
+ `/patients/janedoe` went out whole. A route template (`/patients/:id/notes`, `{id}`, `<int:id>`,
9
+ `[slug]`, `*path`) is kept as it is. This applies everywhere strict privacy names a request or a
10
+ call by its path: span names and transaction names.
11
+
3
12
  ## 0.18.0 (2026-10-20)
4
13
 
5
14
  - New `config.strict_privacy` (off by default, or `FORGE_OPS_STRICT_PRIVACY=true`) for apps that
data/README.md CHANGED
@@ -243,8 +243,10 @@ name, an email or a diagnosis:
243
243
  - Context and tags only keep the top-level keys you list in `privacy_allowed_fields`, and only
244
244
  when the value is a string, number or boolean. Nothing else from them is sent.
245
245
  - 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.
246
+ - A `transaction_name` that is a raw path rather than a route or `Controller#action` keeps only
247
+ its first segment (`GET /patients/jane-doe/12` is sent as `GET /patients/:path`; a first
248
+ segment with a digit becomes `:id`), in error events, performance samples, failure events and
249
+ spans alike. A route template such as `/patients/:id/notes` is kept as it is.
248
250
  - Outbound HTTP spans are named by method and host only (`GET api.example.com`), and span data
249
251
  keeps only the masked `db.statement` and `db.system`.
250
252
 
@@ -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
@@ -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.0"
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ForgeOps