Skip to content

Commit be2631d

Browse files
author
Sentience Dev
committed
Merge pull request #78 from SentienceAPI/upload_indexing
Trace index upload
2 parents 78e3f5d + 0ee9939 commit be2631d

File tree

3 files changed

+394
-7
lines changed

3 files changed

+394
-7
lines changed

src/tracing/cloud-sink.ts

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,10 @@ export class CloudTraceSink extends TraceSink {
284284
if (statusCode === 200) {
285285
console.log('✅ [Sentience] Trace uploaded successfully');
286286

287-
// Call /v1/traces/complete to report file sizes (NEW)
287+
// Upload trace index file
288+
await this._uploadIndex();
289+
290+
// Call /v1/traces/complete to report file sizes
288291
await this._completeTrace();
289292

290293
// 4. Delete temp file on success
@@ -381,6 +384,170 @@ export class CloudTraceSink extends TraceSink {
381384
}
382385
}
383386

387+
/**
388+
* Upload trace index file to cloud storage.
389+
*
390+
* Called after successful trace upload to provide fast timeline rendering.
391+
* The index file enables O(1) step lookups without parsing the entire trace.
392+
*/
393+
private async _uploadIndex(): Promise<void> {
394+
// Construct index file path (same as trace file with .index.json extension)
395+
const indexPath = this.tempFilePath.replace('.jsonl', '.index.json');
396+
397+
try {
398+
// Check if index file exists
399+
await fsPromises.access(indexPath);
400+
} catch {
401+
this.logger?.warn('Index file not found, skipping index upload');
402+
return;
403+
}
404+
405+
try {
406+
// Request index upload URL from API
407+
if (!this.apiKey) {
408+
this.logger?.info('No API key provided, skipping index upload');
409+
return;
410+
}
411+
412+
const uploadUrlResponse = await this._requestIndexUploadUrl();
413+
if (!uploadUrlResponse) {
414+
return;
415+
}
416+
417+
// Read and compress index file
418+
const indexData = await fsPromises.readFile(indexPath);
419+
const compressedIndex = zlib.gzipSync(indexData);
420+
const indexSize = compressedIndex.length;
421+
422+
this.logger?.info(`Index file size: ${(indexSize / 1024).toFixed(2)} KB`);
423+
424+
console.log(`📤 [Sentience] Uploading trace index (${indexSize} bytes)...`);
425+
426+
// Upload index to cloud storage
427+
const statusCode = await this._uploadIndexToCloud(uploadUrlResponse, compressedIndex);
428+
429+
if (statusCode === 200) {
430+
console.log('✅ [Sentience] Trace index uploaded successfully');
431+
432+
// Delete local index file after successful upload
433+
try {
434+
await fsPromises.unlink(indexPath);
435+
} catch {
436+
// Ignore cleanup errors
437+
}
438+
} else {
439+
this.logger?.warn(`Index upload failed: HTTP ${statusCode}`);
440+
console.log(`⚠️ [Sentience] Index upload failed: HTTP ${statusCode}`);
441+
}
442+
} catch (error: any) {
443+
// Non-fatal: log but don't crash
444+
this.logger?.warn(`Error uploading trace index: ${error.message}`);
445+
console.log(`⚠️ [Sentience] Error uploading trace index: ${error.message}`);
446+
}
447+
}
448+
449+
/**
450+
* Request index upload URL from Sentience API
451+
*/
452+
private async _requestIndexUploadUrl(): Promise<string | null> {
453+
return new Promise((resolve) => {
454+
const url = new URL(`${this.apiUrl}/v1/traces/index_upload`);
455+
const protocol = url.protocol === 'https:' ? https : http;
456+
457+
const body = JSON.stringify({ run_id: this.runId });
458+
459+
const options = {
460+
hostname: url.hostname,
461+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
462+
path: url.pathname + url.search,
463+
method: 'POST',
464+
headers: {
465+
'Content-Type': 'application/json',
466+
'Content-Length': Buffer.byteLength(body),
467+
Authorization: `Bearer ${this.apiKey}`,
468+
},
469+
timeout: 10000,
470+
};
471+
472+
const req = protocol.request(options, (res) => {
473+
let data = '';
474+
res.on('data', (chunk) => {
475+
data += chunk;
476+
});
477+
res.on('end', () => {
478+
if (res.statusCode === 200) {
479+
try {
480+
const response = JSON.parse(data);
481+
resolve(response.upload_url || null);
482+
} catch {
483+
this.logger?.warn('Failed to parse index upload URL response');
484+
resolve(null);
485+
}
486+
} else {
487+
this.logger?.warn(`Failed to get index upload URL: HTTP ${res.statusCode}`);
488+
resolve(null);
489+
}
490+
});
491+
});
492+
493+
req.on('error', (error) => {
494+
this.logger?.warn(`Error requesting index upload URL: ${error.message}`);
495+
resolve(null);
496+
});
497+
498+
req.on('timeout', () => {
499+
req.destroy();
500+
this.logger?.warn('Index upload URL request timeout');
501+
resolve(null);
502+
});
503+
504+
req.write(body);
505+
req.end();
506+
});
507+
}
508+
509+
/**
510+
* Upload index data to cloud using pre-signed URL
511+
*/
512+
private async _uploadIndexToCloud(uploadUrl: string, data: Buffer): Promise<number> {
513+
return new Promise((resolve, reject) => {
514+
const url = new URL(uploadUrl);
515+
const protocol = url.protocol === 'https:' ? https : http;
516+
517+
const options = {
518+
hostname: url.hostname,
519+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
520+
path: url.pathname + url.search,
521+
method: 'PUT',
522+
headers: {
523+
'Content-Type': 'application/json',
524+
'Content-Encoding': 'gzip',
525+
'Content-Length': data.length,
526+
},
527+
timeout: 30000, // 30 second timeout
528+
};
529+
530+
const req = protocol.request(options, (res) => {
531+
res.on('data', () => {});
532+
res.on('end', () => {
533+
resolve(res.statusCode || 500);
534+
});
535+
});
536+
537+
req.on('error', (error) => {
538+
reject(error);
539+
});
540+
541+
req.on('timeout', () => {
542+
req.destroy();
543+
reject(new Error('Index upload timeout'));
544+
});
545+
546+
req.write(data);
547+
req.end();
548+
});
549+
}
550+
384551
/**
385552
* Get unique identifier for this sink
386553
*/

0 commit comments

Comments
 (0)