@composio/core
Version:

335 lines (330 loc) • 11.4 kB
JavaScript
const require_chunk = require('./chunk-CQwRTUmo.cjs');
const require_ComposioError = require('./ComposioError-CYaI68Mo.cjs');
const require_buffer = require('./buffer-BjWptbNH.cjs');
let _composio_client = require("@composio/client");
//#region src/types/telemetry.types.ts
const TELEMETRY_EVENTS = {
SDK_INITIALIZED: "SDK_INITIALIZED",
SDK_METHOD_INVOKED: "SDK_METHOD_INVOKED",
SDK_METHOD_ERROR: "SDK_METHOD_ERROR",
CLI_INVOKED: "CLI_INVOKED"
};
//#endregion
//#region src/telemetry/BatchProcessor.ts
var BatchProcessor = class {
batch = [];
time;
batchSize;
processBatchCallback;
timer = null;
pendingBatches = /* @__PURE__ */ new Set();
constructor(time = 2e3, batchSize = 100, processBatchCallback) {
this.batch = [];
this.time = time;
this.batchSize = batchSize;
this.processBatchCallback = processBatchCallback;
}
pushItem(item) {
this.batch.push(item);
if (this.batch.length >= this.batchSize) this.processBatch();
else if (!this.timer) this.timer = setTimeout(() => this.processBatch(), this.time);
}
processBatch() {
if (this.batch.length > 0) {
const batchToProcess = this.batch;
this.batch = [];
const pending = this.processBatchCallback(batchToProcess).catch(() => {}).finally(() => {
this.pendingBatches.delete(pending);
});
this.pendingBatches.add(pending);
}
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
/**
* Flush any pending batches and wait for all of them to complete.
* Useful for ensuring telemetry is sent before process exit.
*/
async flush() {
this.processBatch();
if (this.pendingBatches.size > 0) await Promise.all(this.pendingBatches);
}
};
//#endregion
//#region src/services/telemetry/TelemetryService.ts
const TELEMETRY_URL = "https://telemetry.composio.dev/v1";
var TelemetryService = class {
/**
* Sends a metric to the Telemetry API.
* @param payload - The payload to send to the Telemetry API.
* @returns The response from the Telemetry API.
*/
static async sendMetric(payload) {
try {
return await fetch(`${TELEMETRY_URL}/metrics/invocations`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
} catch (error) {
require_ComposioError.logger_default.debug("Error sending metric telemetry", error);
}
}
/**
* Sends an error log to the Telemetry API.
* @param payload - The payload to send to the Telemetry API.
* @returns The response from the Telemetry API.
*/
static async sendErrorLog(payload) {
try {
return await fetch(`${TELEMETRY_URL}/errors`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
} catch (error) {
require_ComposioError.logger_default.debug("Error sending error telemetry", error);
}
}
};
//#endregion
//#region src/telemetry/Telemetry.ts
/**
* The Telemetry class is used to log the telemetry for any given instance which extends InstrumentedInstance.
*
* This class is used to instrument the telemetry for the given instance and send the telemetry to the server.
* This class is also used to create a global error handler for the given instance.
*
* @example
*
* const telemetry = new Telemetry({...});
* const composio = new Composio({...})
*
* telemetry.instrument(composio);
* telemetry.instrument(composio.tools);
* telemetry.instrument(composio.toolkits);
* telemetry.instrument(composio.triggers);
*
*/
var TelemetryTransport = class {
telemetryMetadata;
isTelemetryDisabled = true;
telemetrySource;
telemetrySourceName = "typescript-sdk";
telemetryServiceName = "sdk";
telemetryLanguage = "typescript";
exitHandlersRegistered = false;
batchProcessor = new BatchProcessor(200, 10, async (data) => {
require_ComposioError.logger_default.debug("Sending batch of telemetry metrics", data);
await TelemetryService.sendMetric(data);
});
setup(metadata) {
this.telemetryMetadata = metadata;
this.isTelemetryDisabled = false;
this.telemetrySource = {
host: this.telemetryMetadata?.host ?? this.telemetrySourceName,
service: this.telemetryServiceName,
language: this.telemetryLanguage,
version: this.telemetryMetadata?.version,
platform: this.telemetryMetadata?.isBrowser ? "browser" : "node",
environment: require_ComposioError.getEnvVariable("NODE_ENV", "production")
};
this.registerExitHandlers();
this.sendMetric([{
functionName: TELEMETRY_EVENTS.SDK_INITIALIZED,
durationMs: 0,
timestamp: Date.now() / 1e3,
props: {},
source: this.telemetrySource,
metadata: { provider: this.telemetryMetadata?.provider ?? "openai" },
error: void 0
}]);
}
/**
* Instrument the telemetry for the given instance.
*
* You can pass the instance and the file name of the instance to instrument the telemetry.
* This will instrument all the methods of the instance and log the telemetry for each method call.
* @param instance - any instance that extends InstrumentedInstance
* @param fileName - the file name of the instance
* @returns
*/
instrument(instance, fileName) {
const proto = Object.getPrototypeOf(instance);
const methodNames = Object.getOwnPropertyNames(proto).filter((key) => {
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
return key !== "constructor" && descriptor && typeof descriptor.value === "function" && descriptor.value.constructor.name === "AsyncFunction";
});
const instrumentedClassName = fileName ?? instance.constructor?.name ?? "unknown";
for (const name of methodNames) {
const originalMethod = instance[name];
instance[name] = async (...args) => {
const telemetryEnabled = this.shouldSendTelemetry();
const startTime = telemetryEnabled ? Date.now() : void 0;
try {
const result = await originalMethod.apply(instance, args);
if (telemetryEnabled && startTime !== void 0) {
const durationMs = Date.now() - startTime;
const telemetryPayload = {
functionName: `${instrumentedClassName}.${name}`,
durationMs,
timestamp: startTime / 1e3,
props: {
fileName: instrumentedClassName,
method: name
},
metadata: { provider: this.telemetryMetadata?.provider ?? "openai" },
error: void 0,
source: this.telemetrySource
};
this.batchProcessor.pushItem(telemetryPayload);
}
return result;
} catch (error) {
if (error instanceof Error) {
if (!error.errorId) {
error.errorId = require_buffer.getRandomUUID();
if (telemetryEnabled && startTime !== void 0) {
const durationMs = Date.now() - startTime;
await this.prepareAndSendErrorTelemetry(error, instrumentedClassName, name, startTime, durationMs);
}
}
}
throw error;
}
};
}
return instance;
}
/**
* Check if the telemetry should be sent.
* @returns true if the telemetry should be sent, false otherwise
*/
shouldSendTelemetry() {
const telemetryDisabledEnvironments = ["test", "ci"];
const nodeEnv = (require_ComposioError.getEnvVariable("NODE_ENV", "development") || "").toLowerCase();
const isDisabledEnvironment = telemetryDisabledEnvironments.includes(nodeEnv);
const isTelemetryDisabledByEnv = require_ComposioError.getEnvVariable("TELEMETRY_DISABLED", "false") === "true";
return !this.isTelemetryDisabled && !isTelemetryDisabledByEnv && !isDisabledEnvironment;
}
/**
* Prepare and send the error telemetry.
*
* @TODO This currently blocks the thread and sends the telemetry to the server.
*
* @param {unknown} error - The error to send.
* @param {string} instrumentedClassName - The class name of the instrumented class.
* @param {string} name - The name of the method that threw the error.
* @param {number} startTime - The start time of the method invocation in milliseconds.
* @param {number} durationMs - The duration of the method invocation in milliseconds.
*/
async prepareAndSendErrorTelemetry(error, instrumentedClassName, name, startTime, durationMs) {
const telemetryPayload = {
functionName: `${instrumentedClassName}.${name}`,
durationMs,
timestamp: startTime / 1e3,
props: {
fileName: instrumentedClassName,
method: name
},
metadata: { provider: this.telemetryMetadata?.provider ?? "openai" },
source: this.telemetrySource
};
if (error instanceof _composio_client.ComposioError) telemetryPayload.error = {
errorId: error.errorId,
name: error.name,
message: error.message,
stack: error.stack
};
else if (error instanceof require_ComposioError.ComposioError) telemetryPayload.error = {
errorId: error.errorId,
name: error.name,
code: error.code,
message: error.message,
stack: error.stack
};
else if (error instanceof Error) telemetryPayload.error = {
errorId: error.errorId,
name: error.name ?? "Unknown error",
message: error.message,
stack: error.stack
};
await this.sendErrorTelemetry(telemetryPayload);
}
/**
* Send the telemetry payload to the server.
* @param payload - the telemetry payload to send
* @returns
*/
async sendMetric(payload) {
if (!this.shouldSendTelemetry()) {
require_ComposioError.logger_default.debug("Telemetry is disabled, skipping metric telemetry", payload);
return;
}
try {
require_ComposioError.logger_default.debug("SDK Metric", payload);
await TelemetryService.sendMetric(payload);
} catch (error) {
require_ComposioError.logger_default.error("Error sending metric telemetry", error);
}
}
async sendErrorTelemetry(payload) {
if (!this.shouldSendTelemetry()) {
require_ComposioError.logger_default.debug("Telemetry is disabled, skipping metric telemetry", payload);
return;
}
try {
require_ComposioError.logger_default.debug("SDK Error Telemetry", payload);
await TelemetryService.sendErrorLog(payload);
} catch (error) {
require_ComposioError.logger_default.error("Error sending error telemetry", error);
}
}
/**
* Flush any pending telemetry and wait for it to complete.
* This is automatically called on process exit in Node.js environments.
*/
async flush() {
await this.batchProcessor.flush();
}
/**
* Register process exit handlers to automatically flush telemetry.
* Only registers handlers in Node.js environments (not in browsers).
*/
registerExitHandlers() {
if (this.exitHandlersRegistered || typeof process === "undefined" || !process.on) return;
this.exitHandlersRegistered = true;
const flushSync = () => {
this.flush().catch((error) => {
require_ComposioError.logger_default.debug("Error flushing telemetry on exit", error);
});
};
process.on("beforeExit", () => {
flushSync();
});
const createSignalHandler = (signal) => {
const handler = () => {
require_ComposioError.logger_default.debug(`Received ${signal}, flushing telemetry...`);
this.flush().catch((error) => {
require_ComposioError.logger_default.debug("Error flushing telemetry on signal", error);
}).finally(() => {
process.removeListener(signal, handler);
process.kill(process.pid, signal);
});
};
return handler;
};
process.on("SIGINT", createSignalHandler("SIGINT"));
process.on("SIGTERM", createSignalHandler("SIGTERM"));
}
};
const telemetry = new TelemetryTransport();
//#endregion
Object.defineProperty(exports, 'telemetry', {
enumerable: true,
get: function () {
return telemetry;
}
});