# Changelog

## 3.1.0

- The formatter splits an error apart instead of stringifying it into `message`. `message` holds
  `Class: text`, the frames go to `extra.backtrace`, and the thrower's own properties go to
  `extra.attributes`. An axios error keeps its bare text, as before. `error.cause` follows as
  `caused by:` lines. A parameter after an axios error no longer vanishes.
- Several errors in one call produce one record each. They share the same `message` and differ only
  by `extra.backtrace`, so a count of `log.level: ERROR` now moves with the number of errors, not the
  number of log calls. The common shapes, `console.error(msg, err)` and `console.error(err)`, still
  produce exactly one record.
- A value in `extra.attributes` is cut at 10 000 characters, or at 100 entries for an array, a Set or
  a Map.
- Sanitizers receive `extra.attributes` as an object, so a rule can mask a secret by the key it sits
  under. An axios error's attributes take the same route and are bounded too. One class of value
  escapes this. Anything JSON refuses to serialise, meaning a circular structure or a bigint, arrives
  as a flat string instead.
- `message` is never empty. It falls back to the error's first backtrace line, then to `(no message)`.
- An error's text is no longer read as a format string, so a `%s` a remote service left in it stops
  swallowing the parameter logged after it. A format string the caller wrote still substitutes.
- The browser script reports `unhandledrejection`, and both `/_/log` handlers pass its `trace` into
  `extra.backtrace`. Page errors moved from `window.onerror` to an `error` listener, so the script no
  longer replaces a handler the app installed, nor gets replaced by one installed after it.
- An error logged in the browser leaves as text, stack and a new `attributes` field of the `/_/log`
  body. Both handlers pass that field into `extra.attributes`, so a browser error reaches a sanitizer
  in the same shape a server-side one does. `attributes` and `trace` take the same size limits on the
  way in, since the endpoint accepts an unauthenticated POST. On the page itself an axios error is narrowed to url
  (query stripped), method, request data and response data/status, so its config headers never leave
  the browser. An object that is not an error still goes into the message as before.
- The record carries `client.ip`, `user_agent.original`, `http.request.referrer` and `url.original`,
  filled from the request that opens the scope. Query strings are stripped, and none of it reaches
  Sentry.

## 3.0.0

Requires Node.js >= 22.

### Breaking: the context is scoped to a unit of work

Until 3.0.0 the context was one mutable variable shared by the whole process: concurrent requests,
background jobs, and Lambda invocations reusing a container overwrote each other's context, so a log
line could be stamped with another tenant's project, organization and user. 3.0.0 removes the shared
context entirely — a context now exists only inside a scope, and a scope belongs to one unit of work.

What this means for existing code:

| 2.x | 3.0.0 |
| --- | --- |
| `setContext({ ... })` anywhere | only inside a scope; ignored outside, with a stderr warning naming the call site |
| writing to the `context` export | only inside a scope |
| `contextResolverFromToken(token)` | **removed** — register `contextResolverHook()` on fastify instead |
| `resetContext()` anywhere | inside a scope; a harmless no-op outside |

**Migrating an Express app** — nothing to change, as long as `contextResolverMiddleware()` is mounted
**before** `expressMiddleware()`. Under the reverse order the `/_/log` records land outside any
scope, so check the mounting order when upgrading.

**Migrating a fastify app** — replace the imperative resolver with the new hook:

```javascript
// 2.x
fastify.addHook('onRequest', (req, reply, done) => {
  logsFormatter.contextResolverFromToken(req.query.jwtToken);
  done();
});

// 3.0.0
fastify.addHook('onRequest', logsFormatter.contextResolverHook());
```

**Migrating background work** — cron jobs, queue consumers, workers, scripts — open the scope
yourself with the new `runWithContext(context, fn)`:

```javascript
// 2.x
setContext({ project: { id: 1, identifier: 'newproject' } });
await syncProject();

// 3.0.0
await logsFormatter.runWithContext({ project: { id: 1, identifier: 'newproject' } }, () => syncProject());
```

Everything else keeps its 2.x call signature: `setup`, `contextResolverMiddleware`,
`expressMiddleware`, `applyFastifyRoutes`, `patchConsoleLog`, `registerErrorHandler`, `logToStdout`,
`registerSanitizer`, `clearSanitizers`, and reading the `context` export.

### Breaking: ids are typed as numbers

`project.id`, `organization.id` and `user.id` are declared `number` (and `user.login` a `string`) —
Crowdin ids have always arrived as numbers, and the record now also emits them as numbers on every
path. TypeScript code that passed or read these as strings will need fixing at the source. The
`OtherContext` index signature widened from `Primitive` to `unknown`.

### Added

- `runWithContext(context, fn)` — opens a context scope around a unit of work.
- `contextResolverHook()` — the fastify `onRequest` hook that opens the request's scope.
- `getContext()` — a read-only snapshot of the current scope's context.
- `setup({ appIdentifier })` — the app's identity, set once at startup. It becomes the
  `app_identifier` tag on every Sentry event the process reports — including the app's own captures
  and a crash outside any scope. It is not a field of the stdout/ELK records.
- The `Context` type is exported, so a TypeScript caller can type what it hands to
  `runWithContext()` or `setContext()`.
- `contextResolverMiddleware()` also reads the token from an `Authorization: Bearer` header — that is
  how Crowdin authorises module and app-API calls, which in 2.x got an empty context. Those tokens
  carry no project, so the record falls back to `context.organization` / `context.user` for
  `organization.id` and `user.id`; the project stays the first source when present.
- The injected browser script forwards the page's `?jwtToken` on its `/_/log` calls, so
  browser-side records are attributed to the page's tenant.

### Fixed

- Log records are attributed to the unit of work that wrote them — concurrent requests, webhooks,
  cron jobs and fastify apps no longer overwrite each other's context, including in log calls that
  resume after an `await`.
- A Sentry capture no longer leaves the user or the `app_identifier` tag on the global Sentry scope,
  where they mislabeled whatever was reported next.
