Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
125 changes: 120 additions & 5 deletions src/tracing/cloud-sink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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)
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -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)...`
Expand All @@ -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}`);
Expand All @@ -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<void> {
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
*/
Expand Down
9 changes: 7 additions & 2 deletions src/tracing/tracer-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -172,6 +172,7 @@ function httpPost(url: string, data: any, headers: Record<string, string>): 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
Expand All @@ -194,6 +195,7 @@ export async function createTracer(options: {
apiKey?: string;
runId?: string;
apiUrl?: string;
logger?: SentienceLogger;
}): Promise<Tracer> {
const runId = options.runId || randomUUID();
const apiUrl = options.apiUrl || SENTIENCE_API_URL;
Expand Down Expand Up @@ -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');
Expand Down