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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +4 -2
- data/lib/forge_ops_tracker/performance_flusher.rb +2 -1
- data/lib/forge_ops_tracker/strict_privacy.rb +29 -13
- data/lib/forge_ops_tracker/version.rb +1 -1
- 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: 3d585c1acbde6c8bb06ec05505e21bf4d784974ed8e655c9398fef0c7e595d03
|
|
4
|
+
data.tar.gz: af67478c90a33dd594bbccf712e48dc817d5e99ae6b61edf0a0fca62e816e4ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`
|
|
247
|
-
|
|
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
|
|
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"),
|
|
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
|
|
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} #{
|
|
97
|
+
return "#{method} #{strict_path(target)}" if target.start_with?("/")
|
|
97
98
|
|
|
98
|
-
"#{method} #{host(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)
|
|
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
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|