feat: logging for queries in puter js

This commit is contained in:
Daniel Salazar
2025-09-09 12:08:21 -07:00
parent 8c2e459f3c
commit caa383d5cd
7 changed files with 441 additions and 69 deletions
+4 -1
View File
@@ -43,4 +43,7 @@ jsconfig.json
# node js
# ======================================================================
# the exact tree installed in the node_modules folder
package-lock.json
package-lock.json
AGENTS.md
.roo
+66 -24
View File
@@ -1,31 +1,33 @@
import OS from './modules/OS.js';
import { PuterJSFileSystemModule } from './modules/FileSystem/index.js';
import Hosting from './modules/Hosting.js';
import Apps from './modules/Apps.js';
import UI from './modules/UI.js';
import KV from './modules/KV.js';
import AI from './modules/AI.js';
import Auth from './modules/Auth.js';
import FSItem from './modules/FSItem.js';
import * as utils from './lib/utils.js';
import path from './lib/path.js';
import Util from './modules/Util.js';
import Drivers from './modules/Drivers.js';
import putility from '@heyputer/putility';
import { FSRelayService } from './services/FSRelay.js';
import { FilesystemService } from './services/Filesystem.js';
import { APIAccessService } from './services/APIAccess.js';
import { XDIncomingService } from './services/XDIncoming.js';
import { NoPuterYetService } from './services/NoPuterYet.js';
import { Debug } from './modules/Debug.js';
import { PSocket } from './modules/networking/PSocket.js';
import { PTLSSocket } from "./modules/networking/PTLS.js"
import Threads from './modules/Threads.js';
import Perms from './modules/Perms.js';
import { pFetch } from './modules/networking/requests.js';
import APICallLogger from './lib/APICallLogger.js';
import path from './lib/path.js';
import localStorageMemory from './lib/polyfills/localStorage.js'
import xhrshim from './lib/polyfills/xhrshim.js'
import * as utils from './lib/utils.js';
import AI from './modules/AI.js';
import Apps from './modules/Apps.js';
import Auth from './modules/Auth.js';
import { Debug } from './modules/Debug.js';
import Drivers from './modules/Drivers.js';
import { PuterJSFileSystemModule } from './modules/FileSystem/index.js';
import FSItem from './modules/FSItem.js';
import Hosting from './modules/Hosting.js';
import KV from './modules/KV.js';
import { PSocket } from './modules/networking/PSocket.js';
import { PTLSSocket } from "./modules/networking/PTLS.js"
import { pFetch } from './modules/networking/requests.js';
import OS from './modules/OS.js';
import Perms from './modules/Perms.js';
import Threads from './modules/Threads.js';
import UI from './modules/UI.js';
import Util from './modules/Util.js';
import { WorkersHandler } from './modules/Workers.js';
import { APIAccessService } from './services/APIAccess.js';
import { FilesystemService } from './services/Filesystem.js';
import { FSRelayService } from './services/FSRelay.js';
import { NoPuterYetService } from './services/NoPuterYet.js';
import { XDIncomingService } from './services/XDIncoming.js';
// TODO: This is for a safe-guard below; we should check if we can
// generalize this behavior rather than hard-coding it.
@@ -255,6 +257,11 @@ export default globalThis.puter = (function() {
cat: cat_logger,
});
// Initialize API call logger
this.apiCallLogger = new APICallLogger({
enabled: false // Disabled by default
});
// === START :: Services === //
this.services.register('no-puter-yet', NoPuterYetService);
@@ -585,6 +592,41 @@ export default globalThis.puter = (function() {
document.body.innerHTML += arg;
}
}
/**
* Configures API call logging settings
* @param {Object} config - Configuration options for API call logging
* @param {boolean} config.enabled - Enable/disable API call logging
* @param {boolean} config.enabled - Enable/disable API call logging
*/
configureAPILogging = function(config = {}){
if (this.apiCallLogger) {
this.apiCallLogger.updateConfig(config);
}
return this;
}
/**
* Enables API call logging with optional configuration
* @param {Object} config - Optional configuration to apply when enabling
*/
enableAPILogging = function(config = {}) {
if (this.apiCallLogger) {
this.apiCallLogger.updateConfig({ ...config, enabled: true });
}
return this;
}
/**
* Disables API call logging
*/
disableAPILogging = function() {
if (this.apiCallLogger) {
this.apiCallLogger.disable();
}
return this;
}
}
// Create a new Puter object and return it
+110
View File
@@ -0,0 +1,110 @@
/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* APICallLogger provides centralized logging for all API calls made by the puter-js SDK.
* It logs API calls in a simple format: service - operation - params - result
*/
class APICallLogger {
constructor(config = {}) {
this.config = {
enabled: config.enabled ?? false,
...config
};
}
/**
* Updates the logger configuration
* @param {Object} newConfig - New configuration options
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
}
/**
* Enables API call logging
*/
enable() {
this.config.enabled = true;
}
/**
* Disables API call logging
*/
disable() {
this.config.enabled = false;
}
/**
* Checks if logging is enabled for the current configuration
* @returns {boolean}
*/
isEnabled() {
return this.config.enabled;
}
/**
* Logs the completion of an API request in a simple format
* @param {Object} options - Request completion options
*/
logRequest(options = {}) {
if (!this.isEnabled()) return;
const {
service = 'unknown',
operation = 'unknown',
params = {},
result = null,
error = null
} = options;
// Format params as a readable string
let paramsStr = '{}';
if (params && Object.keys(params).length > 0) {
try {
paramsStr = JSON.stringify(params);
} catch (e) {
paramsStr = '[Unable to serialize params]';
}
}
// Format the log message with bold params
const logMessage = `${service} - ${operation} - \x1b[1m${paramsStr}\x1b[22m`;
if (error) {
console.error(logMessage, { error: error.message || error, result });
} else {
console.log(logMessage, result);
}
}
/**
* Gets current logging statistics
* @returns {Object}
*/
getStats() {
return {
enabled: this.config.enabled,
config: { ...this.config }
};
}
}
export default APICallLogger;
+82 -1
View File
@@ -83,6 +83,17 @@ function initXhr(endpoint, APIOrigin, authToken, method= "post", contentType = "
xhr.setRequestHeader("Authorization", "Bearer " + authToken);
xhr.setRequestHeader("Content-Type", contentType);
xhr.responseType = responseType ?? '';
// Add API call logging if available
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
xhr._puterRequestId = {
method,
service: 'xhr',
operation: endpoint.replace(/^\//, ''),
params: { endpoint, contentType, responseType }
};
}
return xhr;
}
@@ -160,12 +171,35 @@ function handle_error(error_cb, reject_func, error){
function setupXhrEventHandlers(xhr, success_cb, error_cb, resolve_func, reject_func) {
// load: success or error
xhr.addEventListener('load', function(e){
xhr.addEventListener('load', async function(e){
// Log the response if API logging is enabled
if (globalThis.puter?.apiCallLogger?.isEnabled() && this._puterRequestId) {
const response = await parseResponse(this).catch(() => null);
globalThis.puter.apiCallLogger.logRequest({
service: this._puterRequestId.service,
operation: this._puterRequestId.operation,
params: this._puterRequestId.params,
result: this.status >= 400 ? null : response,
error: this.status >= 400 ? { message: this.statusText, status: this.status } : null
});
}
return handle_resp(success_cb, error_cb, resolve_func, reject_func, this, xhr);
});
// error
xhr.addEventListener('error', function(e){
// Log the error if API logging is enabled
if (globalThis.puter?.apiCallLogger?.isEnabled() && this._puterRequestId) {
globalThis.puter.apiCallLogger.logRequest({
service: this._puterRequestId.service,
operation: this._puterRequestId.operation,
params: this._puterRequestId.params,
error: {
message: 'Network error occurred',
event: e.type
}
});
}
return handle_error(error_cb, reject_func, this);
})
}
@@ -247,11 +281,32 @@ async function driverCall_(
contentType = 'application/json;charset=UTF-8',
settings = {},
) {
// Generate request ID for logging
// Store request info for logging
let requestInfo = null;
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
requestInfo = {
interface: driverInterface,
driver: driverName,
method: driverMethod,
args: driverArgs
};
}
// If there is no authToken and the environment is web, try authenticating with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// Log authentication error
if (requestInfo && globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'drivers',
operation: `${driverInterface}::${driverMethod}`,
params: { interface: driverInterface, driver: driverName, method: driverMethod, args: driverArgs },
error: { code: 'auth_canceled', message: 'Authentication canceled' }
});
}
return reject_func({
error: {
code: 'auth_canceled', message: 'Authentication canceled'
@@ -265,6 +320,11 @@ async function driverCall_(
// create xhr object
const xhr = initXhr('/drivers/call', puter.APIOrigin, puter.authToken, 'POST', contentType);
// Store request info for later logging
if (requestInfo) {
xhr._puterDriverRequestInfo = requestInfo;
}
if ( settings.responseType ) {
xhr.responseType = settings.responseType;
}
@@ -370,6 +430,18 @@ async function driverCall_(
return;
}
const resp = await parseResponse(response.target);
// Log driver call response
if (this._puterDriverRequestInfo && globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'drivers',
operation: `${this._puterDriverRequestInfo.interface}::${this._puterDriverRequestInfo.method}`,
params: { interface: this._puterDriverRequestInfo.interface, driver: this._puterDriverRequestInfo.driver, method: this._puterDriverRequestInfo.method, args: this._puterDriverRequestInfo.args },
result: response.status >= 400 || resp?.success === false ? null : resp,
error: response.status >= 400 || resp?.success === false ? resp : null
});
}
// HTTP Error - unauthorized
if(response.status === 401 || resp?.code === "token_auth_failed"){
if(resp?.code === "token_auth_failed" && puter.env === 'web'){
@@ -436,6 +508,15 @@ async function driverCall_(
// error
xhr.addEventListener('error', function(e){
// Log driver call error
if (this._puterDriverRequestInfo && globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'drivers',
operation: `${this._puterDriverRequestInfo.interface}::${this._puterDriverRequestInfo.method}`,
params: { interface: this._puterDriverRequestInfo.interface, driver: this._puterDriverRequestInfo.driver, method: this._puterDriverRequestInfo.method, args: this._puterDriverRequestInfo.args },
error: { message: 'Network error occurred', event: e.type }
});
}
return handle_error(error_cb, reject_func, this);
})
+34 -5
View File
@@ -130,12 +130,41 @@ class Auth{
}
async whoami () {
const resp = await fetch(this.APIOrigin + '/whoami', {
headers: {
Authorization: `Bearer ${this.authToken}`
try {
const resp = await fetch(this.APIOrigin + '/whoami', {
headers: {
Authorization: `Bearer ${this.authToken}`
}
});
const result = await resp.json();
// Log the response
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'auth',
operation: 'whoami',
params: {},
result: result
});
}
});
return await resp.json();
return result;
} catch (error) {
// Log the error
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'auth',
operation: 'whoami',
params: {},
error: {
message: error.message || error.toString(),
stack: error.stack
}
});
}
throw error;
}
}
}
+102 -35
View File
@@ -31,34 +31,73 @@ class FetchDriverCallBackend {
}
async call ({ driver, method_name, parameters }) {
const resp = await fetch(`${this.context.APIOrigin}/drivers/call`, {
headers: {
Authorization: `Bearer ${this.context.authToken}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
'interface': driver.iface_name,
...(driver.service_name
? { service: driver.service_name }
: {}),
method: method_name,
args: parameters,
}),
});
const content_type = resp.headers.get('content-type')
.split(';')[0].trim(); // TODO: parser for Content-Type
const handler = this.response_handlers[content_type];
if ( ! handler ) {
const msg = `unrecognized content type: ${content_type}`;
console.error(msg);
console.error('creating blob so dev tools shows response...');
await resp.blob();
throw new Error(msg);
try {
const resp = await fetch(`${this.context.APIOrigin}/drivers/call`, {
headers: {
Authorization: `Bearer ${this.context.authToken}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
'interface': driver.iface_name,
...(driver.service_name
? { service: driver.service_name }
: {}),
method: method_name,
args: parameters,
}),
});
const content_type = resp.headers.get('content-type')
.split(';')[0].trim(); // TODO: parser for Content-Type
const handler = this.response_handlers[content_type];
if ( ! handler ) {
const msg = `unrecognized content type: ${content_type}`;
console.error(msg);
console.error('creating blob so dev tools shows response...');
await resp.blob();
// Log the error
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'drivers',
operation: `${driver.iface_name}::${method_name}`,
params: { interface: driver.iface_name, driver: driver.service_name || driver.iface_name, method: method_name, args: parameters },
error: { message: msg }
});
}
throw new Error(msg);
}
const result = await handler(resp);
// Log the successful response
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'drivers',
operation: `${driver.iface_name}::${method_name}`,
params: { interface: driver.iface_name, driver: driver.service_name || driver.iface_name, method: method_name, args: parameters },
result: result
});
}
return result;
} catch (error) {
// Log unexpected errors
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'drivers',
operation: `${driver.iface_name}::${method_name}`,
params: { interface: driver.iface_name, driver: driver.service_name || driver.iface_name, method: method_name, args: parameters },
error: {
message: error.message || error.toString(),
stack: error.stack
}
});
}
throw error;
}
return await handler(resp);
}
}
@@ -137,14 +176,42 @@ class Drivers {
}
async list () {
const resp = await fetch(`${this.APIOrigin}/lsmod`, {
headers: {
Authorization: 'Bearer ' + this.authToken,
},
method: 'POST'
});
const list = await resp.json();
return list.interfaces;
try {
const resp = await fetch(`${this.APIOrigin}/lsmod`, {
headers: {
Authorization: 'Bearer ' + this.authToken,
},
method: 'POST'
});
const list = await resp.json();
// Log the response
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'drivers',
operation: 'list',
params: {},
result: list.interfaces
});
}
return list.interfaces;
} catch (error) {
// Log the error
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'drivers',
operation: 'list',
params: {},
error: {
message: error.message || error.toString(),
stack: error.stack
}
});
}
throw error;
}
}
async get (iface_name, service_name) {
@@ -52,9 +52,20 @@ export function pFetch(...args) {
parsedURL.port || 443,
);
} else {
rej(
`Failed to fetch. URL scheme "${parsedURL.protocol}" is not supported.`,
);
const errorMsg = `Failed to fetch. URL scheme "${parsedURL.protocol}" is not supported.`;
// Log the error
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'network',
operation: 'pFetch',
params: { url: reqObj.url, method: reqObj.method },
error: { message: errorMsg }
});
}
rej(errorMsg);
return;
}
// Sending default UA
@@ -198,6 +209,17 @@ export function pFetch(...args) {
chunkedTransfer =
parsedHead.headers.get("transfer-encoding") ===
"chunked";
// Log the response
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'network',
operation: 'pFetch',
params: { url: reqObj.url, method: reqObj.method },
result: { status: parsedHead.status, statusText: parsedHead.statusText }
});
}
// Return initial response object
res(new Response(outStream, parsedHead));
@@ -232,11 +254,29 @@ export function pFetch(...args) {
}
});
socket.on("error", (reason) => {
// Log the error
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'network',
operation: 'pFetch',
params: { url: reqObj.url, method: reqObj.method },
error: { message: "Socket errored with the following reason: " + reason }
});
}
rej("Socket errored with the following reason: " + reason);
});
},
});
} catch (e) {
// Log unexpected errors
if (globalThis.puter?.apiCallLogger?.isEnabled()) {
globalThis.puter.apiCallLogger.logRequest({
service: 'network',
operation: 'pFetch',
params: { url: reqObj.url, method: reqObj.method },
error: { message: e.message || e.toString(), stack: e.stack }
});
}
rej(e);
}});
}