From 3e9d21191136f36d810fd16899a0dc4228f6311a Mon Sep 17 00:00:00 2001 From: rcholic Date: Sun, 28 Dec 2025 17:43:17 -0800 Subject: [PATCH] file size reporting --- package.json | 2 +- src/tracing/cloud-sink.ts | 125 ++++++++++++++++++++++++++++++++-- src/tracing/tracer-factory.ts | 9 ++- 3 files changed, 128 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index daa580a2..b6c397d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sentienceapi", - "version": "0.90.3", + "version": "0.90.5", "description": "TypeScript SDK for Sentience AI Agent Browser Automation", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/tracing/cloud-sink.ts b/src/tracing/cloud-sink.ts index cf3ae45a..67b7f3f7 100644 --- a/src/tracing/cloud-sink.ts +++ b/src/tracing/cloud-sink.ts @@ -10,6 +10,7 @@ */ import * as fs from 'fs'; +import { promises as fsPromises } from 'fs'; import * as os from 'os'; import * as path from 'path'; import * as zlib from 'zlib'; @@ -18,6 +19,15 @@ import * as http from 'http'; import { URL } from 'url'; import { TraceSink } from './sink'; +/** + * Optional logger interface for SDK users + */ +export interface SentienceLogger { + info(message: string): void; + warn(message: string): void; + error(message: string): void; +} + /** * Get persistent cache directory for traces * Uses ~/.sentience/traces/pending/ (survives process crashes) @@ -61,17 +71,36 @@ export class CloudTraceSink extends TraceSink { private runId: string; private writeStream: fs.WriteStream | null = null; private closed: boolean = false; + private apiKey?: string; + private apiUrl: string; + private logger?: SentienceLogger; + + // File size tracking (NEW) + private traceFileSizeBytes: number = 0; + private screenshotTotalSizeBytes: number = 0; /** * Create a new CloudTraceSink * * @param uploadUrl - Pre-signed PUT URL from Sentience API * @param runId - Run ID for persistent cache naming + * @param apiKey - Sentience API key for calling /v1/traces/complete + * @param apiUrl - Sentience API base URL (default: https://api.sentienceapi.com) + * @param logger - Optional logger instance for logging file sizes and errors */ - constructor(uploadUrl: string, runId?: string) { + constructor( + uploadUrl: string, + runId?: string, + apiKey?: string, + apiUrl?: string, + logger?: SentienceLogger + ) { super(); this.uploadUrl = uploadUrl; this.runId = runId || `trace-${Date.now()}`; + this.apiKey = apiKey; + this.apiUrl = apiUrl || 'https://api.sentienceapi.com'; + this.logger = logger; // PRODUCTION FIX: Use persistent cache directory instead of /tmp // This ensures traces survive process crashes! @@ -218,15 +247,30 @@ export class CloudTraceSink extends TraceSink { }); } - // 2. Read and compress trace data - if (!fs.existsSync(this.tempFilePath)) { + // 2. Read and compress trace data (using async operations) + try { + await fsPromises.access(this.tempFilePath); + } catch { console.warn('[CloudTraceSink] Temp file does not exist, skipping upload'); return; } - const traceData = fs.readFileSync(this.tempFilePath); + const traceData = await fsPromises.readFile(this.tempFilePath); const compressedData = zlib.gzipSync(traceData); + // Measure trace file size (NEW) + this.traceFileSizeBytes = compressedData.length; + + // Log file sizes if logger is provided (NEW) + if (this.logger) { + this.logger.info( + `Trace file size: ${(this.traceFileSizeBytes / 1024 / 1024).toFixed(2)} MB` + ); + this.logger.info( + `Screenshot total: ${(this.screenshotTotalSizeBytes / 1024 / 1024).toFixed(2)} MB` + ); + } + // 3. Upload to cloud via pre-signed URL console.log( `📤 [Sentience] Uploading trace to cloud (${compressedData.length} bytes)...` @@ -237,8 +281,11 @@ export class CloudTraceSink extends TraceSink { if (statusCode === 200) { console.log('✅ [Sentience] Trace uploaded successfully'); + // Call /v1/traces/complete to report file sizes (NEW) + await this._completeTrace(); + // 4. Delete temp file on success - fs.unlinkSync(this.tempFilePath); + await fsPromises.unlink(this.tempFilePath); } else { console.error(`❌ [Sentience] Upload failed: HTTP ${statusCode}`); console.error(` Local trace preserved at: ${this.tempFilePath}`); @@ -250,6 +297,74 @@ export class CloudTraceSink extends TraceSink { } } + /** + * Call /v1/traces/complete to report file sizes to gateway. + * + * This is a best-effort call - failures are logged but don't affect upload success. + */ + private async _completeTrace(): Promise { + if (!this.apiKey) { + // No API key - skip complete call + return; + } + + return new Promise((resolve) => { + const url = new URL(`${this.apiUrl}/v1/traces/complete`); + const protocol = url.protocol === 'https:' ? https : http; + + const body = JSON.stringify({ + run_id: this.runId, + stats: { + trace_file_size_bytes: this.traceFileSizeBytes, + screenshot_total_size_bytes: this.screenshotTotalSizeBytes, + }, + }); + + const options = { + hostname: url.hostname, + port: url.port || (url.protocol === 'https:' ? 443 : 80), + path: url.pathname + url.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + Authorization: `Bearer ${this.apiKey}`, + }, + timeout: 10000, // 10 second timeout + }; + + const req = protocol.request(options, (res) => { + // Consume response data + res.on('data', () => {}); + res.on('end', () => { + if (res.statusCode === 200) { + this.logger?.info('Trace completion reported to gateway'); + } else { + this.logger?.warn( + `Failed to report trace completion: HTTP ${res.statusCode}` + ); + } + resolve(); + }); + }); + + req.on('error', (error) => { + // Best-effort - log but don't fail + this.logger?.warn(`Error reporting trace completion: ${error.message}`); + resolve(); + }); + + req.on('timeout', () => { + req.destroy(); + this.logger?.warn('Trace completion request timeout'); + resolve(); + }); + + req.write(body); + req.end(); + }); + } + /** * Get unique identifier for this sink */ diff --git a/src/tracing/tracer-factory.ts b/src/tracing/tracer-factory.ts index a7ad0f84..8b75e848 100644 --- a/src/tracing/tracer-factory.ts +++ b/src/tracing/tracer-factory.ts @@ -16,7 +16,7 @@ import * as http from 'http'; import { URL } from 'url'; import { randomUUID } from 'crypto'; import { Tracer } from './tracer'; -import { CloudTraceSink } from './cloud-sink'; +import { CloudTraceSink, SentienceLogger } from './cloud-sink'; import { JsonlTraceSink } from './jsonl-sink'; /** @@ -172,6 +172,7 @@ function httpPost(url: string, data: any, headers: Record): Prom * @param options.apiKey - Sentience API key (e.g., "sk_pro_xxxxx") * @param options.runId - Unique identifier for this agent run (generates UUID if not provided) * @param options.apiUrl - Sentience API base URL (default: https://api.sentienceapi.com) + * @param options.logger - Optional logger instance for logging file sizes and errors * @returns Tracer configured with appropriate sink * * @example @@ -194,6 +195,7 @@ export async function createTracer(options: { apiKey?: string; runId?: string; apiUrl?: string; + logger?: SentienceLogger; }): Promise { const runId = options.runId || randomUUID(); const apiUrl = options.apiUrl || SENTIENCE_API_URL; @@ -223,7 +225,10 @@ export async function createTracer(options: { console.log('☁️ [Sentience] Cloud tracing enabled (Pro tier)'); // PRODUCTION FIX: Pass runId for persistent cache naming - return new Tracer(runId, new CloudTraceSink(uploadUrl, runId)); + return new Tracer( + runId, + new CloudTraceSink(uploadUrl, runId, options.apiKey, apiUrl, options.logger) + ); } else if (response.status === 403) { console.log('⚠️ [Sentience] Cloud tracing requires Pro tier'); console.log(' Falling back to local-only tracing');