@segment/analytics-next
Version:
Analytics Next (aka Analytics 2.0) is the latest version of Segment’s JavaScript SDK - enabling you to send your data to any tool without having to learn, test, or use a new API every time.
155 lines • 6.24 kB
JavaScript
import { __assign, __awaiter, __generator, __rest } from "tslib";
import { fetch } from '../../lib/fetch';
import { onPageChange } from '../../lib/on-page-change';
import { RateLimitError } from './ratelimit-error';
import { Context } from '../../core/context';
var MAX_PAYLOAD_SIZE = 500;
var MAX_KEEPALIVE_SIZE = 64;
function kilobytes(buffer) {
var size = encodeURI(JSON.stringify(buffer)).split(/%..|./).length - 1;
return size / 1024;
}
/**
* Checks if the payload is over or close to
* the maximum payload size allowed by tracking
* API.
*/
function approachingTrackingAPILimit(buffer) {
return kilobytes(buffer) >= MAX_PAYLOAD_SIZE - 50;
}
/**
* Checks if payload is over or approaching the limit for keepalive
* requests. If keepalive is enabled we want to avoid
* going over this to prevent data loss.
*/
function passedKeepaliveLimit(buffer) {
return kilobytes(buffer) >= MAX_KEEPALIVE_SIZE - 10;
}
function chunks(batch) {
var result = [];
var index = 0;
batch.forEach(function (item) {
var size = kilobytes(result[index]);
if (size >= 64) {
index++;
}
if (result[index]) {
result[index].push(item);
}
else {
result[index] = [item];
}
});
return result;
}
export default function batch(apiHost, config) {
var _a, _b;
var buffer = [];
var pageUnloaded = false;
var limit = (_a = config === null || config === void 0 ? void 0 : config.size) !== null && _a !== void 0 ? _a : 10;
var timeout = (_b = config === null || config === void 0 ? void 0 : config.timeout) !== null && _b !== void 0 ? _b : 5000;
var rateLimitTimeout = 0;
function sendBatch(batch) {
var _a;
if (batch.length === 0) {
return;
}
var writeKey = (_a = batch[0]) === null || _a === void 0 ? void 0 : _a.writeKey;
// Remove sentAt from every event as batching only needs a single timestamp
var updatedBatch = batch.map(function (event) {
var _a = event, sentAt = _a.sentAt, newEvent = __rest(_a, ["sentAt"]);
return newEvent;
});
return fetch("https://".concat(apiHost, "/b"), {
keepalive: (config === null || config === void 0 ? void 0 : config.keepalive) || pageUnloaded,
headers: {
'Content-Type': 'text/plain',
},
method: 'post',
body: JSON.stringify({
writeKey: writeKey,
batch: updatedBatch,
sentAt: new Date().toISOString(),
}),
}).then(function (res) {
var _a;
if (res.status >= 500) {
throw new Error("Bad response from server: ".concat(res.status));
}
if (res.status === 429) {
var retryTimeoutStringSecs = (_a = res.headers) === null || _a === void 0 ? void 0 : _a.get('x-ratelimit-reset');
var retryTimeoutMS = typeof retryTimeoutStringSecs == 'string'
? parseInt(retryTimeoutStringSecs) * 1000
: timeout;
throw new RateLimitError("Rate limit exceeded: ".concat(res.status), retryTimeoutMS);
}
});
}
function flush(attempt) {
var _a;
if (attempt === void 0) { attempt = 1; }
return __awaiter(this, void 0, void 0, function () {
var batch_1;
return __generator(this, function (_b) {
if (buffer.length) {
batch_1 = buffer;
buffer = [];
return [2 /*return*/, (_a = sendBatch(batch_1)) === null || _a === void 0 ? void 0 : _a.catch(function (error) {
var _a;
var ctx = Context.system();
ctx.log('error', 'Error sending batch', error);
if (attempt <= ((_a = config === null || config === void 0 ? void 0 : config.maxRetries) !== null && _a !== void 0 ? _a : 10)) {
if (error.name === 'RateLimitError') {
rateLimitTimeout = error.retryTimeout;
}
buffer.push.apply(buffer, batch_1);
buffer.map(function (event) {
if ('_metadata' in event) {
var segmentEvent = event;
segmentEvent._metadata = __assign(__assign({}, segmentEvent._metadata), { retryCount: attempt });
}
});
scheduleFlush(attempt + 1);
}
})];
}
return [2 /*return*/];
});
});
}
var schedule;
function scheduleFlush(attempt) {
if (attempt === void 0) { attempt = 1; }
if (schedule) {
return;
}
schedule = setTimeout(function () {
schedule = undefined;
flush(attempt).catch(console.error);
}, rateLimitTimeout ? rateLimitTimeout : timeout);
rateLimitTimeout = 0;
}
onPageChange(function (unloaded) {
pageUnloaded = unloaded;
if (pageUnloaded && buffer.length) {
var reqs = chunks(buffer).map(sendBatch);
Promise.all(reqs).catch(console.error);
}
});
function dispatch(_url, body) {
return __awaiter(this, void 0, void 0, function () {
var bufferOverflow;
return __generator(this, function (_a) {
buffer.push(body);
bufferOverflow = buffer.length >= limit ||
approachingTrackingAPILimit(buffer) ||
((config === null || config === void 0 ? void 0 : config.keepalive) && passedKeepaliveLimit(buffer));
return [2 /*return*/, bufferOverflow || pageUnloaded ? flush() : scheduleFlush()];
});
});
}
return {
dispatch: dispatch,
};
}
//# sourceMappingURL=batched-dispatcher.js.map