# Changelog

## 0.12.0 (2026-09-29)

- SQL masking now also catches strings with a backslash-escaped quote (`'o\'brien'`, `E'o\'brien'`), prefixed strings (`E''`, `X''`, `N''`, `B''`, `U&''`, prefix included), hex, binary and exponent numbers (`0x1F`, `0b101`, `3e10`, `1.5E-3`, `.5`), and, on MySQL and MariaDB, `"double-quoted"` strings. `ForgeOps::Tracker::SqlStatement::mask($sql, system => $db_system)` takes the database's name, and a database span's statement is masked with its `db_system`.

## 0.11.0 (2026-09-25)

- A database span can now carry the SQL it ran: `ForgeOps::Tracker::span('Load orders', sub { ... }, kind => 'database', statement => $sql, db_system => 'postgresql')`, plus `record_database_span($name, $sql, $started_at, $duration_ms, db_system => ...)` for a query you timed yourself. The statement is masked (every string and number becomes `?`) and cut to 4000 characters before it's stored, then sent in the span's data as `db.statement`, with `db_system` lowercased as `db.system`. Bind values are never read. Both options are ignored on spans of any other kind, and a `db.statement` put in a database span's `data` directly is masked too.

## 0.10.0 (2026-09-25)

- New `ForgeOps::Tracker::record_change(kind => ..., title => ..., details => {...}, environment => ..., service => ..., actor => ..., url => ..., id => ..., occurred_at => ...)` records something that changed in your system (a feature flag, a config value, a hand-run migration) so ForgeOps can show it next to the errors that followed. `kind` is one of `feature_flag`, `config`, `migration`, `dependency`, `infrastructure`, or `other`; anything else is sent as `other`. Delivered from a background thread like error events, never dies, and does nothing when the client isn't enabled.
- Changes between deploys are now detected automatically. Once per process, `init()` queues a snapshot of the Perl version for the background thread, and ForgeOps records whatever changed since the previous boot. Module versions are left out, since Perl has no reliable record of them. New `detect_changes` option (default 1) turns this off.
- New `track_env_var_names` option (default 0) adds environment variable names, never values, to that snapshot. Host-specific names (`HOSTNAME`, `PATH`, `PORT`, `LC_*`, Kubernetes service variables, and others) and the client's own `FORGE_OPS_*` settings are always left out.

## 0.9.0

- Distributed tracing across services, using the W3C Trace Context standard (`traceparent`). Every request that passes through `PSGIPerformance` or `Dancer2Performance` now gets a trace id; one that arrives with a valid `traceparent` header continues that trace, and its root span points at the caller's span. A missing or malformed header starts a fresh trace. `ForgeOps::Tracker::current_trace_id()` returns it, and `start_trace($traceparent)` continues one for work that isn't a request.
- Every error reported during a request now says where it happened: `trace_id`, plus, under Dancer2, `transaction_name` (the same `GET /users/:id` name performance samples and traces already use) and `endpoint` (the HTTP method plus the route pattern as declared; never the raw path). Set as soon as Dancer2 has matched the route, so an error reported from inside a route already has them. Plain PSGI has no route pattern, so it sends only `trace_id` unless the app names the request with the new `set_request_route($transaction_name, $endpoint)`. Left out outside a request. Structured fields, so they're never PII-scrubbed.
- `ForgeOps::Tracker::http_span($method, $url, sub { my ($headers) = @_; ... }, data => {})` records an outgoing HTTP call as an `http` span named after the method and host, and hands your code the `traceparent` header to add to it; its parent id is the span's own id, so the called service's root span nests under it. This client doesn't instrument an HTTP client itself. New `propagate_traces` (default 1) and `trace_propagation_targets` (default undef, meaning every host; or an arrayref of host strings, each matching that host and its subdomains, and/or `qr//` regular expressions) control where the header goes.
- An errored request's trace is always sent, however fast it was: an error reported while the request ran, or one escaping it. A Dancer2 route that dies now has its trace finished and sent (Dancer2 never runs `after_request` for it, so before this it was never sent at all). Fast, successful requests are unchanged.
- An error escaping a request keeps its trace id, transaction name, endpoint, user and breadcrumbs when it's reported after the performance integration has already finished the request: `Integrations::PSGI` enabled outside `PSGIPerformance` (the documented order), or `Integrations::Dancer2` loaded after `Dancer2Performance`.
- With `track_tracing => 0`, requests still get a trace id (attached to errors and handed out by `http_span`, since that is also what links errors across services); only span reporting stops.
- Trace and span ids were already W3C shaped (32 and 16 lowercase hex characters); they are now also guaranteed never to be all zeros, the one value the standard reserves as invalid.

This client has earlier CPAN releases (see `$ForgeOps::Tracker::VERSION`'s history in source control), but no `CHANGELOG.md` existed for it before this entry; like `sdks/java`'s, it starts here without backfilling earlier versions.