From 4bf6748ba2f18720bf14e8243cfb6661e9332629 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Tue, 10 Nov 2026 15:13:31 -0400 Subject: [PATCH 1/2] fix: release event sender when fetch rejects --- src/xhr.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/xhr.js b/src/xhr.js index 1c1110cc..4e38b02f 100644 --- a/src/xhr.js +++ b/src/xhr.js @@ -61,9 +61,14 @@ Request.prototype.send = function (callback) { responseStatus = response.status; return response.text(); }) - .then((responseText) => { - callback(responseStatus, responseText); - }); + .then( + (responseText) => { + callback(responseStatus, responseText); + }, + () => { + callback(0, ''); + }, + ); } //log('sent request to ' + this.url + ' with data ' + decodeURIComponent(queryString(this.data))); }; From 45e876b75dcf5eb4b400dd715d0c674b23af9d1f Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Tue, 10 Nov 2026 15:18:31 -0400 Subject: [PATCH 2/2] test: cover fetch failures and event upload retries --- test/amplitude-client.js | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/amplitude-client.js b/test/amplitude-client.js index 52d551fc..edea1480 100644 --- a/test/amplitude-client.js +++ b/test/amplitude-client.js @@ -12,6 +12,7 @@ import constants from '../src/constants.js'; import { mockCookie, restoreCookie, getCookie } from './mock-cookie'; import { AmplitudeServerZone } from '../src/server-zone.js'; import Request from '../src/xhr'; +import GlobalScope from '../src/global-scope'; const deleteAllCookies = () => document.cookie.split(';').forEach(function (c) { @@ -1674,6 +1675,52 @@ describe('AmplitudeClient', function () { }); }); + describe('fetch transport failures', function () { + beforeEach(function () { + reset(); + amplitude.init(apiKey); + sandbox.stub(GlobalScope, 'XMLHttpRequest').value(undefined); + }); + + afterEach(function () { + reset(); + }); + + ['fetch', 'response.text'].forEach(function (failure) { + it('should retain events and allow retry after ' + failure + ' rejects', async function () { + const error = new Error('Network failure'); + const fetchStub = sandbox.stub(GlobalScope, 'fetch'); + if (failure === 'fetch') { + fetchStub.onFirstCall().rejects(error); + } else { + fetchStub.onFirstCall().resolves({ status: 200, text: () => Promise.reject(error) }); + } + fetchStub.onSecondCall().resolves({ status: 200, text: () => Promise.resolve('success') }); + + amplitude.logEvent('Retried event'); + const event = amplitude._unsentEvents[0]; + assert.isTrue(amplitude._sending); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.isFalse(amplitude._sending); + assert.lengthOf(amplitude._unsentEvents, 1); + assert.strictEqual(amplitude._unsentEvents[0], event); + assert.isTrue(fetchStub.calledOnce); + + amplitude.sendEvents(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.isTrue(fetchStub.calledTwice); + assert.equal( + queryString.parse(fetchStub.secondCall.args[1].body).e, + queryString.parse(fetchStub.firstCall.args[1].body).e, + ); + assert.isFalse(amplitude._sending); + assert.lengthOf(amplitude._unsentEvents, 0); + }); + }); + }); + describe('logEvent', function () { let clock, startTime;