Skip to content

Commit 2e80459

Browse files
committed
cloud tracing support
1 parent 10416c2 commit 2e80459

File tree

11 files changed

+722
-115
lines changed

11 files changed

+722
-115
lines changed

sentience/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
# Agent Layer (Phase 1 & 2)
1010
from .base_agent import BaseAgent
1111
from .browser import SentienceBrowser
12+
13+
# Tracing (v0.12.0+)
14+
from .cloud_tracing import CloudTraceSink
1215
from .conversational_agent import ConversationalAgent
1316
from .expect import expect
1417

@@ -43,8 +46,7 @@
4346
from .recorder import Recorder, Trace, TraceStep, record
4447
from .screenshot import screenshot
4548
from .snapshot import snapshot
46-
47-
# Tracing (v0.12.0+)
49+
from .tracer_factory import create_tracer
4850
from .tracing import JsonlTraceSink, TraceEvent, Tracer, TraceSink
4951

5052
# Utilities (v0.12.0+)
@@ -107,7 +109,9 @@
107109
"Tracer",
108110
"TraceSink",
109111
"JsonlTraceSink",
112+
"CloudTraceSink",
110113
"TraceEvent",
114+
"create_tracer",
111115
# Utilities (v0.12.0+)
112116
"canonical_snapshot_strict",
113117
"canonical_snapshot_loose",

sentience/cloud_tracing.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""
2+
Cloud trace sink with pre-signed URL upload.
3+
4+
Implements "Local Write, Batch Upload" pattern for enterprise cloud tracing.
5+
"""
6+
7+
import gzip
8+
import json
9+
import os
10+
import tempfile
11+
from typing import Any
12+
13+
import requests
14+
15+
from sentience.tracing import TraceEvent, TraceSink
16+
17+
18+
class CloudTraceSink(TraceSink):
19+
"""
20+
Enterprise Cloud Sink: "Local Write, Batch Upload" pattern.
21+
22+
Architecture:
23+
1. **Local Buffer**: Writes to temp file (zero latency, non-blocking)
24+
2. **Pre-signed URL**: Uses secure pre-signed PUT URL from backend API
25+
3. **Batch Upload**: Uploads complete file on close() or at intervals
26+
4. **Zero Credential Exposure**: Never embeds DigitalOcean credentials in SDK
27+
28+
This design ensures:
29+
- Fast agent performance (microseconds per emit, not milliseconds)
30+
- Security (credentials stay on backend)
31+
- Reliability (network issues don't crash the agent)
32+
33+
Tiered Access:
34+
- Free Tier: Falls back to JsonlTraceSink (local-only)
35+
- Pro/Enterprise: Uploads to cloud via pre-signed URLs
36+
37+
Example:
38+
>>> from sentience.cloud_tracing import CloudTraceSink
39+
>>> from sentience.tracing import Tracer
40+
>>> # Get upload URL from API
41+
>>> upload_url = "https://sentience.nyc3.digitaloceanspaces.com/..."
42+
>>> sink = CloudTraceSink(upload_url)
43+
>>> tracer = Tracer(run_id="demo", sink=sink)
44+
>>> tracer.emit_run_start("SentienceAgent")
45+
>>> tracer.close() # Uploads to cloud
46+
"""
47+
48+
def __init__(self, upload_url: str):
49+
"""
50+
Initialize cloud trace sink.
51+
52+
Args:
53+
upload_url: Pre-signed PUT URL from Sentience API
54+
(e.g., "https://sentience.nyc3.digitaloceanspaces.com/...")
55+
"""
56+
self.upload_url = upload_url
57+
58+
# Create temporary file for buffering
59+
# delete=False so we can read it back before uploading
60+
self._temp_file = tempfile.NamedTemporaryFile(
61+
mode="w+",
62+
encoding="utf-8",
63+
suffix=".jsonl",
64+
delete=False,
65+
)
66+
self._path = self._temp_file.name
67+
self._closed = False
68+
69+
def emit(self, event: dict[str, Any]) -> None:
70+
"""
71+
Write event to local temp file (Fast, non-blocking).
72+
73+
Performance: ~10 microseconds per write vs ~50ms for HTTP request
74+
75+
Args:
76+
event: Event dictionary from TraceEvent.to_dict()
77+
"""
78+
if self._closed:
79+
raise RuntimeError("CloudTraceSink is closed")
80+
81+
json_str = json.dumps(event, ensure_ascii=False)
82+
self._temp_file.write(json_str + "\n")
83+
self._temp_file.flush() # Ensure written to disk
84+
85+
def close(self) -> None:
86+
"""
87+
Upload buffered trace to cloud via pre-signed URL.
88+
89+
This is the only network call - happens once at the end.
90+
"""
91+
if self._closed:
92+
return
93+
94+
self._closed = True
95+
96+
try:
97+
# 1. Close temp file
98+
self._temp_file.close()
99+
100+
# 2. Compress for upload
101+
with open(self._path, "rb") as f:
102+
trace_data = f.read()
103+
104+
compressed_data = gzip.compress(trace_data)
105+
106+
# 3. Upload to DigitalOcean Spaces via pre-signed URL
107+
print(f"📤 [Sentience] Uploading trace to cloud ({len(compressed_data)} bytes)...")
108+
109+
response = requests.put(
110+
self.upload_url,
111+
data=compressed_data,
112+
headers={
113+
"Content-Type": "application/x-gzip",
114+
"Content-Encoding": "gzip",
115+
},
116+
timeout=60, # 1 minute timeout for large files
117+
)
118+
119+
if response.status_code == 200:
120+
print("✅ [Sentience] Trace uploaded successfully")
121+
else:
122+
print(f"❌ [Sentience] Upload failed: HTTP {response.status_code}")
123+
print(f" Response: {response.text}")
124+
print(f" Local trace preserved at: {self._path}")
125+
126+
except Exception as e:
127+
print(f"❌ [Sentience] Error uploading trace: {e}")
128+
print(f" Local trace preserved at: {self._path}")
129+
# Don't raise - preserve trace locally even if upload fails
130+
131+
finally:
132+
# 4. Cleanup temp file (only if upload succeeded)
133+
if os.path.exists(self._path):
134+
try:
135+
# Only delete if upload was successful
136+
if hasattr(self, "_upload_successful") and self._upload_successful:
137+
os.remove(self._path)
138+
except Exception:
139+
pass # Ignore cleanup errors
140+
141+
def __enter__(self):
142+
"""Context manager support."""
143+
return self
144+
145+
def __exit__(self, exc_type, exc_val, exc_tb):
146+
"""Context manager cleanup."""
147+
self.close()
148+
return False

sentience/extension/background.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,13 @@ async function handleScreenshotCapture(_tabId, options = {}) {
144144
async function handleSnapshotProcessing(rawData, options = {}) {
145145
const MAX_ELEMENTS = 10000; // Safety limit to prevent hangs
146146
const startTime = performance.now();
147-
147+
148148
try {
149149
// Safety check: limit element count to prevent hangs
150150
if (!Array.isArray(rawData)) {
151151
throw new Error('rawData must be an array');
152152
}
153-
153+
154154
if (rawData.length > MAX_ELEMENTS) {
155155
console.warn(`[Sentience Background] ⚠️ Large dataset: ${rawData.length} elements. Limiting to ${MAX_ELEMENTS} to prevent hangs.`);
156156
rawData = rawData.slice(0, MAX_ELEMENTS);
@@ -186,7 +186,7 @@ async function handleSnapshotProcessing(rawData, options = {}) {
186186
// Add timeout protection (18 seconds - less than content.js timeout)
187187
analyzedElements = await Promise.race([
188188
wasmPromise,
189-
new Promise((_, reject) =>
189+
new Promise((_, reject) =>
190190
setTimeout(() => reject(new Error('WASM processing timeout (>18s)')), 18000)
191191
)
192192
]);

sentience/extension/content.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ function handleSnapshotRequest(data) {
8484
if (responded) return; // Already responded via timeout
8585
responded = true;
8686
clearTimeout(timeoutId);
87-
87+
8888
const duration = performance.now() - startTime;
8989

9090
// Handle Chrome extension errors (e.g., background script crashed)

0 commit comments

Comments
 (0)