Repository navigation
Releases: quonfig/sdk-javascript
Releases · quonfig/sdk-javascript
Release list
v1.4.0
1.4.0 - 2026-10-09
- Fix: a failed poll tick is no longer an unhandled promise rejection. During an outage with no
last-known-good entry, every tick rejected with no handler: Node exits on the first one, and
browsers report each to error trackers. The loop now keeps the current config, warns once per
outage, and keeps polling. - Fix: one malformed evaluation no longer hangs
init()or stops polling. A bad entry (ajson
value sent as a string, or a null value) made the whole payload fail inside the loader, which then
never settled. Each key is now decoded on its own: the bad key returnsundefinedand
getDetailsreportsreason: "ERROR",errorCode: "TYPE_MISMATCH"(the same path as a malformed
duration), and the other keys load. A load also always settles if installing a result throws. - Fix: the per-leg fetch
timeoutcovers the response body. It was cleared when headers
arrived, so a server that sent headers and then stalled hung the load with no deadline. - Fix: a load replaced by
updateContext()can no longer install the previous context's values
(qfg-goi1.2.7). WhenupdateContext()raced an in-flight refresh or poll tick, a leg of the
older load that still completed was installed under the old context, so the client could serve
user A's flags whilecontextssaid user B. The aborted older load also counted as a primary
failure and fired the secondary leg, and its hedge timer kept running. A replaced load now does
nothing: no install, no secondary leg, no last-known-good, and its promise settles with the load
that replaced it instead of rejecting with the abort. - Fix:
updateContext(ctx, true)followed bypoll()installs the new context's values
(qfg-goi1.2.48).skipLoadchanged the context but not the one the loader fetched for, and
poll()did not sync it, so the first poll fetch was for the previous context and was installed,
and written to the last-known-good cache, as the new one's. At the same generation the later,
correct fetches were dropped as "not newer", so user B saw user A's flags until the next publish,
and an offline reload for B served A's values as stale.poll()andupdateContext(ctx, true)
now point the loader at the current context, and every leg of a load (and its last-known-good
fallback) is for the context the load started with. - Fix:
close()orstopPolling()during the firstpoll()fetch no longer restarts polling.
A secondpoll()call also no longer leaves two loops running. - Fix: config keys that match
Object.prototypenames (for exampleconstructor) are no longer
found when absent;getDetailsreportsFLAG_NOT_FOUND. - Fix:
base64Encode(used for the context in the fetch URL) is UTF-8 in every runtime. Without
TextEncoder(React Native) it produced Latin-1 for accented text and threw above U+00FF; without
window(Web/Service Workers, Cloudflare Workers) it threw. Output on modern browsers and Node is
unchanged. - Fix: importing the SDK no longer requires
crypto.getRandomValues. The telemetry instance id
is created on first use and falls back to a non-cryptographic random id. - Fix: the last-known-good cache keeps one entry per SDK key. It stored one localStorage entry
per distinct context and never removed any, which could fill the origin's quota (breaking the
app's ownlocalStoragewrites) and kept the raw context in the key. The entry is now
quonfig.lkg.v2:<sdkKey>with a hash of the context, served only for the same context. Old
quonfig.lkg.v1:<sdkKey>:*entries are removed on the first write. - Change:
shouldLog({loggerPath})no longer addsquonfig-sdk-loggingto the client context.
It changed every later fetch's context, which let an older config generation replace a newer one
and changed the inputs to every flag. The browser evaluates server-side once per fetched context,
so the logger path never selected per-logger rules. Logger paths from the browser no longer appear
in example contexts. - Packaging: removed the
modulefield (it nameddist/index.mjs, which was never built;
bundlers fall back tomain) and thelintscript (eslint is not installed).
v1.3.0
- Telemetry transport policy (qfg-y8je.11). Telemetry POSTs get their own 10s deadline
(telemetryTimeoutMs); the eval-fetchtimeoutoption no longer applies to telemetry. A failed
batch (timeout, network error, 408, 429, 5xx) is kept in memory for the life of the page and
resent byte-for-byte (never merged, so the server dedups a resend of a batch that did land), no
sooner than 30s after the failure and after any readableRetry-After(up to 10 min). The
retained queue is capped at 5 batches / 512KB / 5 min (oldest dropped; a batch larger than the
byte cap is never kept). At most one POST is in flight. 401/403/404 disable telemetry for the page
with one error; any other 4xx drops that batch with one error. Logging: a failed POST logs at
debug, oneconsole.warnwhen data is actually dropped (then a summary at most every 10 min), one
console.infoon recovery. Before this, every non-2xx logged a warning and every failed batch was
lost. - Fix: parallel telemetry POSTs shared one abort timer, so one request finishing cleared the
other's deadline (it could then hang with no timeout). Each request now owns its timer, cleared
when it settles. - Fix: a telemetry network error was an unhandled promise rejection. It is now caught and
treated as retryable. - Fix:
close()left the telemetry timer armed when the final POST failed (it rejected before
stopping timers).close()now stops polling and the telemetry timer first, sends the current
window once with a 2s deadline andkeepalive, never rejects, and does not resend kept batches. pagehidereplacesbeforeunloadfor the final flush: the live window is sent once with
fetch(..., { keepalive: true })and a 2s deadline without blocking unload.close()removes the
listener.- Flush interval is now an option (
telemetryFlushIntervalMs, default 30s, unchanged). - Memory cap: evaluation-summary keys per window drop from 100,000 to 10,000
(telemetryMaxEvaluationSummaries), the uniform cap across SDKs; a key already in the window
keeps counting at the cap. - New options:
telemetryFlushIntervalMs,telemetryTimeoutMs,telemetryMaxRetainedBatches,
telemetryMaxRetainedBytes,telemetryMaxRetainedAgeMs,telemetryMaxEvaluationSummaries.
Invalid values (non-finite or <= 0) fall back to the default. TelemetryUploader(thetelemetryUploaderaccessor) gainssend();post()keeps its behavior
with a per-request deadline, andclearAbortTimeout()is a deprecated no-op. No wire change, no
removed public API, no new dependencies.
v1.2.1
- Fix: eval-summary telemetry counters now report
reasonas the numeric wire code (1=STATIC 2=TARGETING_MATCH 3=SPLIT), matching the backend SDKs. Previously the counter carried the OpenFeature-style string form (e.g."TARGETING_MATCH"), which the telemetry ingestion API's stricter validation could reject — evaluation-summary batches containing a rule-matched evaluation could be dropped as a result. The ingestion service now also accepts and normalizes the string form, so existing 0.0.16–1.2.0 clients work without upgrading; this release aligns the SDK with the canonical wire contract. Counters without a reason are unchanged (field omitted). No API changes.
v1.2.0
init()warns when an explicitapiUrlsdisables failover. The default (and everydomain/
QUONFIG_DOMAIN-derived) API-URL list carries BOTH a primary and a secondary leg, and the SDK
hedges/fails over between them (§5e). An explicitapiUrls(or the singularapiUrlalias)
replaces that list wholesale, so a single-entry override silently dropped the secondary and
disabled automatic failover.init()now logs a one-line WARN pointing the caller at the fix
(pass both a primary and a secondary URL). The default two-leg list never warns. Diagnostic only;
no behavior change, no new dependencies. New READMEFailover & QUONFIG_DOMAINsection documents
the URL derivation and the failover model.init()warns whentimeout <= hedgeDelay. The per-leg fetchtimeoutmust stay above
hedgeDelay(default 2000ms): if it doesn't, the primary leg is aborted before the hedge timer
can fire, so the parallel hedge (§5e) silently degrades to error-only sequential failover — the
secondary is only contacted after the primary fully times out, never concurrently with a
still-alive-but-slow primary.init()now logs a clear warning naming both effective values, and
thetimeout/hedgeDelayoption jsdoc documents the invariant. Diagnostic only; no behavior
change.- Telemetry upload timeout restored to 10s. 1.1.0's hedge lowered the shared per-request timeout
to 3s for the eval fetch, which also silently clipped background telemetry POSTs from 10s to 3s
(they shared oneDEFAULT_TIMEOUT). Telemetry now uses its ownTELEMETRY_TIMEOUT(10s), so a
slow-but-alive telemetry endpoint no longer drops shape/example data that the eval-latency budget
was never meant to govern. No API change; thetimeoutinit option still overrides it.
v1.1.0
Secondary-delivery failover release (qfg-7h5d.2). See CHANGELOG.md: parallel hedge (loader.ts), reject-older install guard via Meta.generation incl. the gen<=0 carve-out (defensive), last-known-good localStorage cache. Peer ranges kept at >=1.0.0. Backward-compatible minor.
v1.0.0
v0.0.18
- Conditional polling: the loader now sends
If-None-Matchon repeat eval-with-context polls and
honors a304 Not Modifiedby keeping the cached evaluations instead of re-downloading the full
payload. The ETag is stored per-request-URL (which embeds the encoded context) and LRU-bounded to
16 entries, so a context switch can never replay a stale ETag. Steady-state polling collapses to a
304 when neither the workspace version nor the context has changed (qfg-iikt). - Fix: a 304 now returns the payload cached for that exact context, so the
updateContext(A)→
updateContext(B)→updateContext(A)pattern can no longer leave the previous context's
evaluations in the single shared config slot and serve the wrong context's values (qfg-iikt). - Fix: polling now starts and self-heals even when the very first poll fetch rejects (a startup
network blip with both primary and secondary briefly unreachable). Previously a rejected bootstrap
fetch left polling permanently dead — config frozen with no recovery after connectivity returned.
The recurring loop is now scheduled regardless of the first fetch's outcome, matching the
steady-state loop's own resilience (qfg-8uw5).
v0.0.17
- Breaking (typing-level): removed the
collectLoggerNamesinit option and its internal
LoggerAggregator. The server-side telemetry pipeline never consumed the logger-name event (no
schema entry, no flatten branch, no ClickHouse table), so this was dead client-side cost.
TypeScript callers passingcollectLoggerNames: true | falsewill get a type error — drop the
field (qfg-o2fk). Logger-level evaluation viashouldLog({loggerPath, ...})is unchanged; logger
paths still flow to the dashboard through the existing example-context telemetry.
v0.0.16
- Added
getDetails<T>accessor that returns the resolved value alongside the evaluationreason,
variant, andflagMetadatafor richer client-side telemetry and debugging (qfg-ez8e). - Declared
engines.node >= 20.9.0and added a CI matrix to match the supported Node floor across
the Quonfig SDK family (qfg-y7xh). - Added Prettier as the repo formatter and wired up a CI gate so unformatted commits fail the push.