# Changelog

## 2.0.0 (2026-10-22)

Implements [avohq/spec-first-inspector-server-sdk](https://github.com/avohq/spec-first-inspector-server-sdk) v3.0.1: the `/inspector/v2/track` endpoint, batching with `flush()` and `destroy()`, gzip, and gateway fields. This is a breaking release; see [Upgrading from 1.x to 2.0](README.md#upgrading-from-1x-to-20) in the README.

Breaking: the track call.

- **`trackSchemaFromEvent` takes one `InspectorEvent` object**, `trackSchemaFromEvent({ eventName, eventProperties, streamId?, outputReference?, originHint?, originAppVersion? })`, and it is the only form: the 1.x positional `trackSchemaFromEvent(eventName, eventProperties, streamId?)` is removed. `InspectorEvent` is exported as a TypeScript type. The wire output for the same values is unchanged. A call whose argument is not an object (such as an unchanged 1.x call from JavaScript) sends nothing, resolves `[]`, and prints an always-on, rate-limited error on stderr naming the fix. `eventProperties` may be absent or `null` (an event with no properties). Avo Codegen's internal entry point keeps its call shape.
- **Unknown `InspectorEvent` keys are warned about.** A key other than the six above (a typo such as `eventname` or `outputRef`) prints a warning on stderr naming the key, whatever the logging flag, at most once per 10 seconds. Only key names are printed, never values; the event is still tracked with the known keys.

Event spec validation (dev and staging):

- **Property values are checked against every value constraint in the event spec.** 1.x checked only the property type and the first regex pattern. Values are now checked against pinned values, allowed values, regex patterns and min/max ranges, on each property and on the properties of an object or of each object in a list (one level deep), with the same rules as the web and Android SDKs. A value outside a property's allowed values now shows up as a value issue in the Inspector Debugger.
- **Every event and variant in the spec is checked.** 1.x used only the first event the endpoint returned and ignored variants. Each constraint applies to the event and variant IDs the spec gives it, so a value can fail for one variant and pass for another.
- **Failures are reported against tracking-plan event IDs.** 1.x reported a failure against the Codegen event id, or the event name for a manual track, which the Debugger cannot match to an event. The wire now carries the failed event and variant IDs from the spec, and object properties' failures in their `children`. `passedEventIds` is no longer sent.
- **The property type is no longer compared with the spec.** Avo already compares the reported schema with the tracking plan, and the spec's type names differ from the schema's (`bool` and `boolean`, a list's element type and `list(…)`), so 1.x reported false failures for booleans, lists and whole numbers sent for `float` properties. A value of the wrong type still fails a constraint it cannot meet: a regex needs a string, a range a number.
- **Validation is bounded.** It runs synchronously in the track call, so a list of more than 1,000 items is not checked, and a string longer than 10,000 characters is not checked against regex patterns (each logged when logging is on). The event is still sent.
- **Gateway events are validated against the spec at their checkpoint.** An event tracked with `originHint` and/or `outputReference` now passes them to the event spec fetch (the same trimmed values as on the track body; a blank one is not sent), so a gateway key answers the spec of the gateway-input checkpoint (`originHint`) or of that output (`originHint` and `outputReference`). Each checkpoint's spec is cached and fetched on its own. A fetch without either is unchanged, and source keys ignore them. A gateway key answers 404 without `originHint`, and the event is then sent without validation, as for any spec it cannot fetch. A failed spec fetch is now cached, so it is not repeated for every event: a 404 (no spec, such as a gateway key without `originHint`) for the spec cache's 60 seconds, and any other failure (timeout, other status, network error, unparseable body) for 5 seconds, so a blip skips validation only briefly. Unlike a fetched spec, a cached failure is not refetched after 50 uses. ([AVO-4070](https://linear.app/avo/issue/AVO-4070))

Input handling. All of these behaved the same way in 1.x:

- **Null-prototype objects no longer wipe the schema.** A null-prototype object anywhere in the event properties (for example `querystring.parse()` output), or an object with its own `hasOwnProperty` key, made schema extraction fail: the event was sent with an empty schema and a stack trace was printed. These objects are now extracted exactly like plain objects, and deduplication, event spec validation and encryption handle them too.
- **A non-string `streamId` no longer rejects.** A number, bigint or boolean (a numeric user id, say) is sent as its string form. Any other non-string is treated as absent, with a warning when logging is on. Before, the track promise rejected, and a call without `.catch()` crashed the process with an unhandled rejection.
- **A non-string `env` falls back to `dev`** with a warning instead of throwing `value.trim is not a function`.
- **Non-whole numbers in exponent form are `float`.** `1e-7` and `5e-324` were classified `int`. A number is now `int` when it is a whole number and `float` otherwise; `NaN` and `±Infinity` are `float`.
- **Float typed arrays are `list(float)`.** A `Float32Array` or `Float64Array` is reported as `list(float)` with children `["float"]`, as the Java and Go SDKs type `float[]`/`double[]` and `[]float32`/`[]float64`, so the same data has the same type as a plain `[0.5, 1.5]`. Every other typed array, `Buffer` and `DataView` stays `list(int)` with `["int"]`. In 1.x a typed array was walked index by index and reported as an object with one property per element.
- **Equal list elements are reported once.** A list's children hold each distinct element schema once, in first-occurrence order, compared by value: objects with the same property names and types are equal whatever their property order, and nested lists compare their children in order. 1.x collapsed equal type strings but kept one child per object or nested list element, so a list of 1,000 similar objects carried 1,000 identical children. This matches the Java and Go SDKs.
- **A `null` list element is typed `"null"`.** `{ v: [null, 1] }` now has children `["null", "int"]`, following the spec's schema pseudocode; 1.x reported the null element as `[]`.
- **Invalid constructor arguments throw the documented messages.** A non-string `apiKey`, missing options (`new AvoInspector()`) or `null` options throw "[Avo Inspector] No API key provided…", and a non-string `version` throws "[Avo Inspector] No version provided…", instead of a `TypeError`.
- **Logs never contain property values.** With logging on, 1.x printed each event's raw properties ("Supplied event … with params …"), and because the logging flag is shared by every instance, a `dev` instance could make a `prod` one print user data such as emails. Log lines now show event names and the extracted schema (property names and types), never property values or the API key; event and property names are still printed, so they should not contain personal data.
- **Lost data is always logged.** Events dropped because the buffer or send backlog is full, because a send failed with an internal error, or because the process exited before they were sent (or before their send completed), batches rejected with a non-200 status (1.x did not log these at all), send failures and internal errors are printed on stderr whatever the logging flag, at most one line per kind per 10 seconds; the rest are counted and reported later (with the next occurrence, by `flush()` after the window, or at once by `destroy()` and at exit), worded by the real time they cover. Sampling drops and debug lines still need logging on.
- **An event without a name is sent as "Missing Event Name".** A `null`, `undefined`, non-string, empty or whitespace-only event name was sent as is (or with no `eventName` key at all). Such an event is now sent under the name `"Missing Event Name"`, with its full schema, in every env and without throwing, and an always-on, rate-limited line reports it. A valid name is sent unchanged, surrounding whitespace included.
- `package.json` declares `"types"` and `"engines": { "node": ">=14" }`.

Robustness under load:

- **At most 4 batches are sent at once.** Further batches wait their turn, and up to 10,000 events can wait; beyond that the oldest waiting events are dropped, and the drop is logged, so a fast producer or a slow or unresponsive endpoint cannot open thousands of requests, and the number of events held for sending stays bounded. `maxQueueSize` bounds only the unsent buffer. `flush()` waits for waiting batches, and `destroy()` discards them. An awaited track waits for a send slot once 1,000 events are waiting (backpressure, at most 10 s per call), which slows an awaited loop, such as an unchanged 1.x backfill script, down to the speed of the sends. Backpressure slows the backlog's growth but does not guarantee delivery: if the Inspector API is slow or down for long, the backlog can still fill and the oldest waiting events are dropped (logged). Calls that are not awaited are not slowed down (see the README).
- **The exit drain is bounded.** In-flight requests do not hold the process open. On `beforeExit` the SDK attempts to send everything left under a single 10-second deadline, and holds the process no longer than that for its own drain (plus up to 1 s for events still waiting for their event spec, and up to 10 s for un-awaited tracks still waiting on a full send backlog); other work in your process can still keep it running longer. Events still waiting for their spec halfway through are sent without validation, and whatever could not be sent is reported at exit. An awaited track call keeps the process alive until it settles (at most 30 s per call, before any exit drain), so an awaited loop waiting on spec fetches or dev sends is not taken for the exit; a call still pending after 30 s (such as a batch-size-1 send still waiting for a send slot) resolves with its schema so the script carries on, and its event stays queued for the exit drain or is reported as unsent at exit; this applies to any call whose promise is observed (`await`, `.then`, `.catch`, `.finally`, `Promise.all`), so a fire-and-forget `track(...).catch(() => {})` in dev or staging holds the process within the same bounds. Only a call whose promise nothing touches holds nothing.
- **Schema extraction is bounded.** It stops expanding a value more than 10 levels deep, a value that contains itself, and anything past 10,000 objects and lists in one call; such a value is reported as `"object"`. At most 10,000 properties are reported per call, and binary data (a `Buffer`, typed array or `ArrayBuffer`) is reported by type without visiting its bytes: a 1 MiB Buffer used to produce a million-property schema, and larger ones crashed the process. In 1.x a cyclic or very deep payload made extraction fail and the event was sent with an empty schema. See "Schema extraction limits" in the README.
- **A response cut off mid-body is judged by its status.** A 200 whose body ends early (the connection closes before the full `Content-Length`) counts as delivered, like a 200 with an unparseable body: the sampling rate is unchanged and no failed send is logged. A cut-off non-200 counts as that non-200. This matches the Java and Go SDKs.
- **Lower CPU cost per event.** The Codegen/manual deduplicator's cleanup no longer scans every recent timestamp on each call, which took most of the CPU time under load. As a side effect, a registration that shared its millisecond with another now expires after 500 ms like any other.
- **`flush()` reports whether the instance drained.** `flush(timeoutMs?)` resolves `true` when the instance has nothing buffered, waiting or in flight, and `false` when the timeout passed first. It still never rejects, and resolves `true` after `destroy()`. A backfill script can flush until drained, with a bounded number of attempts (see the README).
- **`flush()` during event spec validation sends batches.** Events whose validations finish together are sent as one batch, not one request per event.
- **One shared connection pool for event spec fetches.** Every instance reuses one keep-alive agent (at most 8 sockets, 2 kept idle), so creating instances without destroying them no longer adds a connection pool, and its idle connections, per instance.
- **Event spec fetches queued for a connection keep their full 10 s budget, and at most 1,000 events wait for a spec at once.** The 10 s deadline starts when a fetch gets a connection, not when it is requested, so fetches queued behind slow ones are no longer timed out unsent; a fetch that gets no connection within 10 s (for example behind 8 hung ones) is abandoned and its event sent without validation. Past 1,000 waiting events (across every instance), further events are sent without validation instead of waiting.
