# Changelog

## 0.12.3 (2026-10-03)

- `IO::Socket::SSL` (1.42+) and `Net::SSLeay` (1.49+) are now prerequisites, so `cpanm ForgeOps::Tracker` installs them. `HTTP::Tiny` needs both to send over https, which a ForgeOps DSN always is, and neither is core Perl: on a perl without them the client installed cleanly and then delivered nothing, without saying why.
- When the DSN is https and this perl can't make https requests, `init()` now logs one line, once per process, through `logger` (or `warn` when no logger is set): `[ForgeOps] Not sending: the DSN is https, and this perl can't make https requests. Install IO::Socket::SSL and Net::SSLeay (cpanm IO::Socket::SSL Net::SSLeay) to send.`

## 0.12.2 (2026-10-02)

- `cpanm ForgeOps::Tracker` now passes its own tests on a clean Perl. The tests' local HTTP server needs `HTTP::Server::PSGI`, and several tests use `Plack::Test`, but only `Test::More` was listed as a test requirement, so the install failed. `Plack` and `HTTP::Message` (already one of Plack's prerequisites) are now in `TEST_REQUIRES` and the cpanfile's test requirements. They are still not runtime dependencies. Dancer2 stays optional: the Dancer2 integration tests, and the Dancer2 part of the trace context tests, now skip when it isn't installed.
- With a DSN set and an environment that isn't enabled, `init()` now logs one line, once per process, through `logger` (or `warn` when no logger is set): `[ForgeOps] Not sending: this environment is "development", and only production, staging are enabled. Set FORGE_OPS_ENVIRONMENT=production (or add "development" to the enabled environments) to send from here.` Nothing is logged without a DSN. The environment default is unchanged: `FORGE_OPS_ENVIRONMENT`, else `production`.

## 0.12.1 (2026-10-01)

- A script that reports an error and exits straight away no longer loses it. When the program ends normally, the client now sends whatever errors, changes and traces are still waiting and lets a delivery already under way finish, for at most 5 seconds. A program that never reported anything doesn't wait at all, and the program's exit status is kept.
- New `ForgeOps::Tracker::flush($timeout)` sends everything still waiting right now and returns 1 once it has gone out (timeout in seconds, default 5). Use it before `POSIX::_exit` or `exec`, which skip the automatic send.

## 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.