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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +6 -2
- data/lib/forge_ops_tracker/performance_flusher.rb +2 -1
- data/lib/forge_ops_tracker/query_explainer.rb +2 -1
- data/lib/forge_ops_tracker/strict_privacy.rb +29 -13
- data/lib/forge_ops_tracker/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a07d319688ef560a28c82ff70a822a2a171d1fe9d4c47973d51093e528e548e2
|
|
4
|
+
data.tar.gz: 6272b6ee64197d04a056a7d34ac4fe5de069be87dea1992f32d30700164ab95a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`
|
|
247
|
-
|
|
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
|
|
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
|
-
|
|
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"),
|
|
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
|
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.
|
|
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
|