Context
packages/loopover-miner/lib/orb-export.js's sendAmsExportBatch (lines 180-201) POSTs the anonymized
telemetry batch straight to the AMS collector with no request timeout at all:
export async function sendAmsExportBatch({ batch, secret, collectorUrl = resolveAmsCollectorUrl(), collectorToken, fetchFn = fetch }) {
if (!Array.isArray(batch) || batch.length === 0) return { sent: 0 };
const instanceId = amsInstanceId(secret);
const body = JSON.stringify({ instanceId, events: batch });
const signature = createHmac("sha256", secret).update(body).digest("hex");
try {
const res = await fetchFn(collectorUrl, {
method: "POST",
headers: { /* ... */ },
body,
});
if (!res.ok) return { sent: 0, error: `http_${res.status}` };
} catch (error) {
return { sent: 0, error: describeCliError(error) };
}
return { sent: batch.length };
}
There is no AbortSignal.timeout(...) (or equivalent AbortController) anywhere in this call — a stalled
TCP connection or a collector that accepts the connection but never responds will hang this fetchFn call
forever. sendAmsExportBatch is invoked synchronously (awaited) from runOrbExportCli's --send path
(packages/loopover-miner/lib/orb-export.js, runOrbExportCli), so a hang here hangs the entire
loopover-miner orb export --enable --send CLI invocation, including any unattended/scheduled use of it.
Every other external-fetch adapter in this package already bounds its request with a timeout:
lib/http-retry.js's fetchWithRetry (fetchOnce) always issues a fresh AbortSignal.timeout(timeoutMs)
per attempt — used by opportunity-fanout.js's githubGetJson and discovery-index-client.js's
queryDiscoveryIndex/submitSoftClaim.
lib/live-issue-snapshot.js's fetchLiveIssueSnapshot passes signal: AbortSignal.timeout(requestTimeoutMs)
directly.
lib/update-check.js's fetchLatestPackageVersion uses a manual AbortController + setTimeout(() => controller.abort(), ...).
sendAmsExportBatch is the one external-fetch call site in this package with no bound at all — the same
caliber of gap already fixed for contribution-profile-extract.js, self-review-context.js's
fetchRepoFocusManifestFile, and the extension's screenshot fetch (fetchShotContentBlock) in this exact
gap-audit series, none of which cover orb-export.js.
Requirements
sendAmsExportBatch in packages/loopover-miner/lib/orb-export.js must bound its fetchFn(collectorUrl, ...) call with signal: AbortSignal.timeout(timeoutMs), added to the existing init object passed to
fetchFn (do not change the method/headers/body fields already there).
- Add a
timeoutMs parameter to sendAmsExportBatch's destructured options object, defaulting to a new
exported constant DEFAULT_ORB_EXPORT_TIMEOUT_MS (module-level const, value 10_000, matching this
package's other default request timeouts in live-issue-snapshot.js/opportunity-fanout.js).
runOrbExportCli does not need to pass timeoutMs explicitly — the default must apply automatically to
every real (non-test) invocation.
- A timeout must be caught by the existing
try { ... } catch (error) { return { sent: 0, error: describeCliError(error) }; } block exactly like any other thrown fetch error — no new catch branch is
needed, AbortSignal.timeout's abort surfaces as a thrown DOMException/AbortError that the existing
catch already handles.
- Do not add retry logic (
fetchWithRetry) — this is strictly about bounding the single request with a
timeout, matching the narrower fix already applied to the other single-fetch adapters named above (e.g.
live-issue-snapshot.js), not the retrying adapters (http-retry.js's consumers).
Deliverables
Test Coverage Requirements
This repo's Codecov patch gate (codecov/patch) enforces target: 99%, threshold: 0%, branch-counted, on
every changed line/branch under src/**/packages/** — this fix is inside packages/** and is gated. Add
the regression test described above so the new timeout wiring is exercised, not just the existing success/
non-2xx/thrown-error branches. Measure locally with npm run test:coverage (unsharded).
Expected Outcome
sendAmsExportBatch (and therefore loopover-miner orb export --enable --send) can no longer hang
indefinitely on a stalled or non-responding AMS collector connection — a request that exceeds
DEFAULT_ORB_EXPORT_TIMEOUT_MS (or a caller-supplied timeoutMs) resolves with { sent: 0, error: ... }
the same way any other transport failure already does, bringing this adapter in line with every other
external-fetch call site in the package.
Links & Resources
packages/loopover-miner/lib/orb-export.js (the file to fix — sendAmsExportBatch, lines ~180-201)
packages/loopover-miner/lib/http-retry.js's fetchOnce and packages/loopover-miner/lib/live-issue-snapshot.js's fetchLiveIssueSnapshot for the established AbortSignal.timeout pattern to mirror
packages/loopover-miner/lib/update-check.js's fetchLatestPackageVersion for the AbortController variant of the same discipline
Context
packages/loopover-miner/lib/orb-export.js'ssendAmsExportBatch(lines 180-201) POSTs the anonymizedtelemetry batch straight to the AMS collector with no request timeout at all:
There is no
AbortSignal.timeout(...)(or equivalentAbortController) anywhere in this call — a stalledTCP connection or a collector that accepts the connection but never responds will hang this
fetchFncallforever.
sendAmsExportBatchis invoked synchronously (awaited) fromrunOrbExportCli's--sendpath(
packages/loopover-miner/lib/orb-export.js,runOrbExportCli), so a hang here hangs the entireloopover-miner orb export --enable --sendCLI invocation, including any unattended/scheduled use of it.Every other external-fetch adapter in this package already bounds its request with a timeout:
lib/http-retry.js'sfetchWithRetry(fetchOnce) always issues a freshAbortSignal.timeout(timeoutMs)per attempt — used by
opportunity-fanout.js'sgithubGetJsonanddiscovery-index-client.js'squeryDiscoveryIndex/submitSoftClaim.lib/live-issue-snapshot.js'sfetchLiveIssueSnapshotpassessignal: AbortSignal.timeout(requestTimeoutMs)directly.
lib/update-check.js'sfetchLatestPackageVersionuses a manualAbortController+setTimeout(() => controller.abort(), ...).sendAmsExportBatchis the one external-fetch call site in this package with no bound at all — the samecaliber of gap already fixed for
contribution-profile-extract.js,self-review-context.js'sfetchRepoFocusManifestFile, and the extension's screenshot fetch (fetchShotContentBlock) in this exactgap-audit series, none of which cover
orb-export.js.Requirements
sendAmsExportBatchinpackages/loopover-miner/lib/orb-export.jsmust bound itsfetchFn(collectorUrl, ...)call withsignal: AbortSignal.timeout(timeoutMs), added to the existinginitobject passed tofetchFn(do not change themethod/headers/bodyfields already there).timeoutMsparameter tosendAmsExportBatch's destructured options object, defaulting to a newexported constant
DEFAULT_ORB_EXPORT_TIMEOUT_MS(module-levelconst, value10_000, matching thispackage's other default request timeouts in
live-issue-snapshot.js/opportunity-fanout.js).runOrbExportClidoes not need to passtimeoutMsexplicitly — the default must apply automatically toevery real (non-test) invocation.
try { ... } catch (error) { return { sent: 0, error: describeCliError(error) }; }block exactly like any other thrown fetch error — no new catch branch isneeded,
AbortSignal.timeout's abort surfaces as a thrownDOMException/AbortErrorthat the existingcatchalready handles.fetchWithRetry) — this is strictly about bounding the single request with atimeout, matching the narrower fix already applied to the other single-fetch adapters named above (e.g.
live-issue-snapshot.js), not the retrying adapters (http-retry.js's consumers).Deliverables
sendAmsExportBatchpassessignal: AbortSignal.timeout(timeoutMs)on itsfetchFncall.DEFAULT_ORB_EXPORT_TIMEOUT_MSconstant (10_000) is the default when the caller doesnot supply
timeoutMs.fetchFnstub which never resolves (or an injected already-abortedsignal / a
fetchFnthat rejects with anAbortError) causessendAmsExportBatchto resolve with{ sent: 0, error: ... }rather than hanging, and that the real default timeout value is exercised(e.g. asserting the
init.signalpassed to a stubbedfetchFnis anAbortSignal).Test Coverage Requirements
This repo's Codecov patch gate (
codecov/patch) enforcestarget: 99%, threshold: 0%, branch-counted, onevery changed line/branch under
src/**/packages/**— this fix is insidepackages/**and is gated. Addthe regression test described above so the new timeout wiring is exercised, not just the existing success/
non-2xx/thrown-error branches. Measure locally with
npm run test:coverage(unsharded).Expected Outcome
sendAmsExportBatch(and thereforeloopover-miner orb export --enable --send) can no longer hangindefinitely on a stalled or non-responding AMS collector connection — a request that exceeds
DEFAULT_ORB_EXPORT_TIMEOUT_MS(or a caller-suppliedtimeoutMs) resolves with{ sent: 0, error: ... }the same way any other transport failure already does, bringing this adapter in line with every other
external-fetch call site in the package.
Links & Resources
packages/loopover-miner/lib/orb-export.js(the file to fix —sendAmsExportBatch, lines ~180-201)packages/loopover-miner/lib/http-retry.js'sfetchOnceandpackages/loopover-miner/lib/live-issue-snapshot.js'sfetchLiveIssueSnapshotfor the establishedAbortSignal.timeoutpattern to mirrorpackages/loopover-miner/lib/update-check.js'sfetchLatestPackageVersionfor theAbortControllervariant of the same discipline