diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 6a6df19347be50ff53ee8d85fa96900b9a42ed47..95fe61b49a889de1287f73852946238c70d2572c 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -825,7 +825,6 @@ function workspaceEndpoints(app) { : "Forked Thread", }); - await Telemetry.sendTelemetry("thread_forked"); await EventLogs.logEvent( "thread_forked", { diff --git a/server/models/telemetry.js b/server/models/telemetry.js index 011da05e97f444229d926a14d5a675aabc5c1a41..101fb58e0f026234fdf1117232ab70a27be90a7c 100644 --- a/server/models/telemetry.js +++ b/server/models/telemetry.js @@ -1,11 +1,28 @@ const { v4 } = require("uuid"); const { SystemSettings } = require("./systemSettings"); +// Map of events and last sent time to check if the event is on cooldown +// This will be cleared on server restart - but that is fine since it is mostly to just +// prevent spamming the logs. +const TelemetryCooldown = new Map(); + const Telemetry = { // Write-only key. It can't read events or any of your other data, so it's safe to use in public apps. pubkey: "phc_9qu7QLpV8L84P3vFmEiZxL020t2EqIubP7HHHxrSsqS", stubDevelopmentEvents: true, // [DO NOT TOUCH] Core team only. label: "telemetry_id", + /* + Key value pairs of events that should be debounced to prevent spamming the logs. + This should be used for events that could be triggered in rapid succession that are not useful to atomically log. + The value is the number of seconds to debounce the event + */ + debounced: { + agent_chat_started: 1800, + sent_chat: 1800, + agent_chat_sent: 1800, + agent_chat_started: 1800, + agent_tool_call: 1800, + }, id: async function () { const result = await SystemSettings.get({ label: this.label }); @@ -34,6 +51,34 @@ const Telemetry = { return "other"; }, + /** + * Checks if the event is on cooldown + * @param {string} event - The event to check + * @returns {boolean} - True if the event is on cooldown, false otherwise + */ + isOnCooldown: function (event) { + // If the event is not debounced, return false + if (!this.debounced[event]) return false; + + // If the event is not in the cooldown map, return false + const lastSent = TelemetryCooldown.get(event); + if (!lastSent) return false; + + // If the event is in the cooldown map, check if it has expired + const now = Date.now(); + const cooldown = this.debounced[event] * 1000; + return now - lastSent < cooldown; + }, + + /** + * Marks the event as on cooldown - will check if the event is debounced first + * @param {string} event - The event to mark + */ + markOnCooldown: function (event) { + if (!this.debounced[event]) return; + TelemetryCooldown.set(event, Date.now()); + }, + sendTelemetry: async function ( event, eventProperties = {}, @@ -46,6 +91,9 @@ const Telemetry = { const distinctId = !!subUserId ? `${systemId}::${subUserId}` : systemId; const properties = { ...eventProperties, runtime: this.runtime() }; + // If the event is on cooldown, return + if (this.isOnCooldown(event)) return; + // Silence some events to keep logs from being too messy in production // eg: Tool calls from agents spamming the logs. if (!silent) { @@ -63,6 +111,9 @@ const Telemetry = { }); } catch { return; + } finally { + // Mark the event as on cooldown if needed + this.markOnCooldown(event); } },