seitime-frappe/frappe/public/js/telemetry/pulse.js
2026-01-03 21:08:51 +05:30

147 lines
3.1 KiB
JavaScript

class PulseProvider {
constructor() {
this.enabled = false;
this.eq = null;
}
is_enabled() {
return frappe.boot.telemetry_provider?.includes("pulse") && frappe.boot.enable_telemetry;
}
init() {
if (!this.is_enabled()) return;
this.enabled = true;
try {
this.eq = new QueueManager((events) => this.sendEvents(events), {
flushInterval: 10000,
});
// Send remaining events on unload
window.addEventListener("beforeunload", () => {
const events = this.eq?.getBufferedEvents?.() || [];
if (events.length) this.sendBeacon(events);
});
} catch (error) {
// ignore errors
}
}
capture(event, app, props) {
if (!this.enabled) return;
this.eq.add({
event_name: event,
app: app,
properties: props,
user: frappe.session.user,
captured_at: new Date().toISOString(),
});
}
sendEvents(events) {
// Return a Promise so QueueManager can retry on failure.
return new Promise((resolve, reject) => {
try {
frappe.call({
method: "frappe.utils.telemetry.pulse.client.bulk_capture",
args: { events },
type: "POST",
no_spinner: true,
freeze: false,
callback: () => resolve(),
error: (error) => reject(error),
});
} catch (error) {
reject(error);
}
});
}
sendBeacon(events) {
try {
if (navigator.sendBeacon) {
const url = "/api/method/frappe.utils.telemetry.pulse.client.bulk_capture";
const data = new FormData();
data.append("events", JSON.stringify(events));
navigator.sendBeacon(url, data);
}
} catch (error) {
// ignore errors
}
}
}
class QueueManager {
constructor(flushCallback, options = {}) {
this.flushCallback = flushCallback;
this.queue = [];
this.pendingBatch = null;
this.retryAttempts = 0;
this.maxRetries = 3;
this.maxQueueSize = options.maxQueueSize || 20;
this.flushInterval = options.flushInterval || 5000;
this.timer = null;
this.flushing = false;
this.start();
}
getBufferedEvents() {
const events = [];
if (this.pendingBatch?.length) events.push(...this.pendingBatch);
if (this.queue.length) events.push(...this.queue);
return events;
}
start() {
this.timer = setInterval(() => {
if (this.queue.length || this.pendingBatch) this.flush();
}, this.flushInterval);
}
add(event) {
this.queue.push(event);
if (this.queue.length >= this.maxQueueSize) {
this.flush();
}
}
async flush() {
if (this.flushing) return;
this.flushing = true;
try {
if (!this.pendingBatch) {
if (!this.queue.length) return;
this.pendingBatch = this.queue.splice(0, this.maxQueueSize);
this.retryAttempts = 0;
}
try {
await this.flushCallback(this.pendingBatch);
this.pendingBatch = null;
this.retryAttempts = 0;
} catch (error) {
this.retryAttempts++;
if (this.retryAttempts > this.maxRetries) {
this.pendingBatch = null;
this.retryAttempts = 0;
}
}
} finally {
this.flushing = false;
}
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.flush();
}
}
export const pulse_provider = new PulseProvider();