@predicatelabs/sdk 0.99.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/README.md +252 -0
- package/dist/actions.d.ts +185 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +1120 -0
- package/dist/actions.js.map +1 -0
- package/dist/agent-runtime.d.ts +352 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +1170 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/agent.d.ts +164 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +408 -0
- package/dist/agent.js.map +1 -0
- package/dist/asserts/expect.d.ts +159 -0
- package/dist/asserts/expect.d.ts.map +1 -0
- package/dist/asserts/expect.js +547 -0
- package/dist/asserts/expect.js.map +1 -0
- package/dist/asserts/index.d.ts +58 -0
- package/dist/asserts/index.d.ts.map +1 -0
- package/dist/asserts/index.js +70 -0
- package/dist/asserts/index.js.map +1 -0
- package/dist/asserts/query.d.ts +199 -0
- package/dist/asserts/query.d.ts.map +1 -0
- package/dist/asserts/query.js +288 -0
- package/dist/asserts/query.js.map +1 -0
- package/dist/backends/actions.d.ts +119 -0
- package/dist/backends/actions.d.ts.map +1 -0
- package/dist/backends/actions.js +291 -0
- package/dist/backends/actions.js.map +1 -0
- package/dist/backends/browser-use-adapter.d.ts +131 -0
- package/dist/backends/browser-use-adapter.d.ts.map +1 -0
- package/dist/backends/browser-use-adapter.js +219 -0
- package/dist/backends/browser-use-adapter.js.map +1 -0
- package/dist/backends/cdp-backend.d.ts +66 -0
- package/dist/backends/cdp-backend.d.ts.map +1 -0
- package/dist/backends/cdp-backend.js +273 -0
- package/dist/backends/cdp-backend.js.map +1 -0
- package/dist/backends/index.d.ts +80 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +101 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/protocol.d.ts +156 -0
- package/dist/backends/protocol.d.ts.map +1 -0
- package/dist/backends/protocol.js +16 -0
- package/dist/backends/protocol.js.map +1 -0
- package/dist/backends/sentience-context.d.ts +143 -0
- package/dist/backends/sentience-context.d.ts.map +1 -0
- package/dist/backends/sentience-context.js +359 -0
- package/dist/backends/sentience-context.js.map +1 -0
- package/dist/backends/snapshot.d.ts +188 -0
- package/dist/backends/snapshot.d.ts.map +1 -0
- package/dist/backends/snapshot.js +360 -0
- package/dist/backends/snapshot.js.map +1 -0
- package/dist/browser.d.ts +154 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +920 -0
- package/dist/browser.js.map +1 -0
- package/dist/canonicalization.d.ts +126 -0
- package/dist/canonicalization.d.ts.map +1 -0
- package/dist/canonicalization.js +161 -0
- package/dist/canonicalization.js.map +1 -0
- package/dist/captcha/strategies.d.ts +12 -0
- package/dist/captcha/strategies.d.ts.map +1 -0
- package/dist/captcha/strategies.js +43 -0
- package/dist/captcha/strategies.js.map +1 -0
- package/dist/captcha/types.d.ts +45 -0
- package/dist/captcha/types.d.ts.map +1 -0
- package/dist/captcha/types.js +12 -0
- package/dist/captcha/types.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +422 -0
- package/dist/cli.js.map +1 -0
- package/dist/conversational-agent.d.ts +123 -0
- package/dist/conversational-agent.d.ts.map +1 -0
- package/dist/conversational-agent.js +341 -0
- package/dist/conversational-agent.js.map +1 -0
- package/dist/cursor-policy.d.ts +41 -0
- package/dist/cursor-policy.d.ts.map +1 -0
- package/dist/cursor-policy.js +81 -0
- package/dist/cursor-policy.js.map +1 -0
- package/dist/debugger.d.ts +28 -0
- package/dist/debugger.d.ts.map +1 -0
- package/dist/debugger.js +107 -0
- package/dist/debugger.js.map +1 -0
- package/dist/expect.d.ts +16 -0
- package/dist/expect.d.ts.map +1 -0
- package/dist/expect.js +67 -0
- package/dist/expect.js.map +1 -0
- package/dist/failure-artifacts.d.ts +95 -0
- package/dist/failure-artifacts.d.ts.map +1 -0
- package/dist/failure-artifacts.js +805 -0
- package/dist/failure-artifacts.js.map +1 -0
- package/dist/generator.d.ts +16 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +205 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -0
- package/dist/inspector.d.ts +13 -0
- package/dist/inspector.d.ts.map +1 -0
- package/dist/inspector.js +153 -0
- package/dist/inspector.js.map +1 -0
- package/dist/llm-provider.d.ts +144 -0
- package/dist/llm-provider.d.ts.map +1 -0
- package/dist/llm-provider.js +460 -0
- package/dist/llm-provider.js.map +1 -0
- package/dist/ordinal.d.ts +90 -0
- package/dist/ordinal.d.ts.map +1 -0
- package/dist/ordinal.js +249 -0
- package/dist/ordinal.js.map +1 -0
- package/dist/overlay.d.ts +63 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +102 -0
- package/dist/overlay.js.map +1 -0
- package/dist/protocols/browser-protocol.d.ts +79 -0
- package/dist/protocols/browser-protocol.d.ts.map +1 -0
- package/dist/protocols/browser-protocol.js +9 -0
- package/dist/protocols/browser-protocol.js.map +1 -0
- package/dist/query.d.ts +66 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +482 -0
- package/dist/query.js.map +1 -0
- package/dist/read.d.ts +47 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +128 -0
- package/dist/read.js.map +1 -0
- package/dist/recorder.d.ts +44 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +262 -0
- package/dist/recorder.js.map +1 -0
- package/dist/runtime-agent.d.ts +72 -0
- package/dist/runtime-agent.d.ts.map +1 -0
- package/dist/runtime-agent.js +357 -0
- package/dist/runtime-agent.js.map +1 -0
- package/dist/screenshot.d.ts +17 -0
- package/dist/screenshot.d.ts.map +1 -0
- package/dist/screenshot.js +40 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/snapshot-diff.d.ts +23 -0
- package/dist/snapshot-diff.d.ts.map +1 -0
- package/dist/snapshot-diff.js +119 -0
- package/dist/snapshot-diff.js.map +1 -0
- package/dist/snapshot.d.ts +47 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +358 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/textSearch.d.ts +64 -0
- package/dist/textSearch.d.ts.map +1 -0
- package/dist/textSearch.js +113 -0
- package/dist/textSearch.js.map +1 -0
- package/dist/tools/context.d.ts +18 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +40 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/defaults.d.ts +5 -0
- package/dist/tools/defaults.d.ts.map +1 -0
- package/dist/tools/defaults.js +368 -0
- package/dist/tools/defaults.js.map +1 -0
- package/dist/tools/filesystem.d.ts +12 -0
- package/dist/tools/filesystem.d.ts.map +1 -0
- package/dist/tools/filesystem.js +137 -0
- package/dist/tools/filesystem.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +38 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +100 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tracing/cloud-sink.d.ts +189 -0
- package/dist/tracing/cloud-sink.d.ts.map +1 -0
- package/dist/tracing/cloud-sink.js +1067 -0
- package/dist/tracing/cloud-sink.js.map +1 -0
- package/dist/tracing/index-schema.d.ts +231 -0
- package/dist/tracing/index-schema.d.ts.map +1 -0
- package/dist/tracing/index-schema.js +235 -0
- package/dist/tracing/index-schema.js.map +1 -0
- package/dist/tracing/index.d.ts +12 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +28 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/tracing/indexer.d.ts +20 -0
- package/dist/tracing/indexer.d.ts.map +1 -0
- package/dist/tracing/indexer.js +347 -0
- package/dist/tracing/indexer.js.map +1 -0
- package/dist/tracing/jsonl-sink.d.ts +51 -0
- package/dist/tracing/jsonl-sink.d.ts.map +1 -0
- package/dist/tracing/jsonl-sink.js +329 -0
- package/dist/tracing/jsonl-sink.js.map +1 -0
- package/dist/tracing/sink.d.ts +25 -0
- package/dist/tracing/sink.d.ts.map +1 -0
- package/dist/tracing/sink.js +15 -0
- package/dist/tracing/sink.js.map +1 -0
- package/dist/tracing/tracer-factory.d.ts +102 -0
- package/dist/tracing/tracer-factory.d.ts.map +1 -0
- package/dist/tracing/tracer-factory.js +375 -0
- package/dist/tracing/tracer-factory.js.map +1 -0
- package/dist/tracing/tracer.d.ts +140 -0
- package/dist/tracing/tracer.d.ts.map +1 -0
- package/dist/tracing/tracer.js +336 -0
- package/dist/tracing/tracer.js.map +1 -0
- package/dist/tracing/types.d.ts +203 -0
- package/dist/tracing/types.d.ts.map +1 -0
- package/dist/tracing/types.js +8 -0
- package/dist/tracing/types.js.map +1 -0
- package/dist/types.d.ts +422 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/action-executor.d.ts +25 -0
- package/dist/utils/action-executor.d.ts.map +1 -0
- package/dist/utils/action-executor.js +121 -0
- package/dist/utils/action-executor.js.map +1 -0
- package/dist/utils/browser-evaluator.d.ts +76 -0
- package/dist/utils/browser-evaluator.d.ts.map +1 -0
- package/dist/utils/browser-evaluator.js +130 -0
- package/dist/utils/browser-evaluator.js.map +1 -0
- package/dist/utils/browser.d.ts +30 -0
- package/dist/utils/browser.d.ts.map +1 -0
- package/dist/utils/browser.js +75 -0
- package/dist/utils/browser.js.map +1 -0
- package/dist/utils/element-filter.d.ts +76 -0
- package/dist/utils/element-filter.d.ts.map +1 -0
- package/dist/utils/element-filter.js +195 -0
- package/dist/utils/element-filter.js.map +1 -0
- package/dist/utils/grid-utils.d.ts +37 -0
- package/dist/utils/grid-utils.d.ts.map +1 -0
- package/dist/utils/grid-utils.js +283 -0
- package/dist/utils/grid-utils.js.map +1 -0
- package/dist/utils/llm-interaction-handler.d.ts +41 -0
- package/dist/utils/llm-interaction-handler.d.ts.map +1 -0
- package/dist/utils/llm-interaction-handler.js +171 -0
- package/dist/utils/llm-interaction-handler.js.map +1 -0
- package/dist/utils/llm-response-builder.d.ts +56 -0
- package/dist/utils/llm-response-builder.d.ts.map +1 -0
- package/dist/utils/llm-response-builder.js +130 -0
- package/dist/utils/llm-response-builder.js.map +1 -0
- package/dist/utils/selector-utils.d.ts +12 -0
- package/dist/utils/selector-utils.d.ts.map +1 -0
- package/dist/utils/selector-utils.js +32 -0
- package/dist/utils/selector-utils.js.map +1 -0
- package/dist/utils/snapshot-event-builder.d.ts +28 -0
- package/dist/utils/snapshot-event-builder.d.ts.map +1 -0
- package/dist/utils/snapshot-event-builder.js +88 -0
- package/dist/utils/snapshot-event-builder.js.map +1 -0
- package/dist/utils/snapshot-processor.d.ts +27 -0
- package/dist/utils/snapshot-processor.d.ts.map +1 -0
- package/dist/utils/snapshot-processor.js +47 -0
- package/dist/utils/snapshot-processor.js.map +1 -0
- package/dist/utils/trace-event-builder.d.ts +122 -0
- package/dist/utils/trace-event-builder.d.ts.map +1 -0
- package/dist/utils/trace-event-builder.js +365 -0
- package/dist/utils/trace-event-builder.js.map +1 -0
- package/dist/utils/trace-file-manager.d.ts +70 -0
- package/dist/utils/trace-file-manager.d.ts.map +1 -0
- package/dist/utils/trace-file-manager.js +194 -0
- package/dist/utils/trace-file-manager.js.map +1 -0
- package/dist/utils/zod.d.ts +5 -0
- package/dist/utils/zod.d.ts.map +1 -0
- package/dist/utils/zod.js +80 -0
- package/dist/utils/zod.js.map +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +13 -0
- package/dist/utils.js.map +1 -0
- package/dist/verification.d.ts +194 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +530 -0
- package/dist/verification.js.map +1 -0
- package/dist/vision-executor.d.ts +18 -0
- package/dist/vision-executor.d.ts.map +1 -0
- package/dist/vision-executor.js +60 -0
- package/dist/vision-executor.js.map +1 -0
- package/dist/visual-agent.d.ts +120 -0
- package/dist/visual-agent.d.ts.map +1 -0
- package/dist/visual-agent.js +796 -0
- package/dist/visual-agent.js.map +1 -0
- package/dist/wait.d.ts +35 -0
- package/dist/wait.d.ts.map +1 -0
- package/dist/wait.js +76 -0
- package/dist/wait.js.map +1 -0
- package/package.json +94 -0
- package/spec/README.md +72 -0
- package/spec/SNAPSHOT_V1.md +208 -0
- package/spec/sdk-types.md +259 -0
- package/spec/snapshot.schema.json +148 -0
- package/src/extension/background.js +104 -0
- package/src/extension/content.js +162 -0
- package/src/extension/injected_api.js +1399 -0
- package/src/extension/manifest.json +36 -0
- package/src/extension/pkg/README.md +1340 -0
- package/src/extension/pkg/package.json +15 -0
- package/src/extension/pkg/sentience_core.d.ts +51 -0
- package/src/extension/pkg/sentience_core.js +371 -0
- package/src/extension/pkg/sentience_core_bg.wasm +0 -0
- package/src/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
- package/src/extension/release.json +116 -0
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CloudTraceSink - Enterprise Cloud Upload
|
|
4
|
+
*
|
|
5
|
+
* Implements "Local Write, Batch Upload" pattern for cloud tracing
|
|
6
|
+
*
|
|
7
|
+
* PRODUCTION HARDENING:
|
|
8
|
+
* - Uses persistent cache directory (~/.sentience/traces/pending/) to survive crashes
|
|
9
|
+
* - Supports non-blocking close() to avoid hanging user scripts
|
|
10
|
+
* - Preserves traces locally on upload failure
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.CloudTraceSink = void 0;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const fs_1 = require("fs");
|
|
49
|
+
const os = __importStar(require("os"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const zlib = __importStar(require("zlib"));
|
|
52
|
+
const https = __importStar(require("https"));
|
|
53
|
+
const http = __importStar(require("http"));
|
|
54
|
+
const url_1 = require("url");
|
|
55
|
+
const sink_1 = require("./sink");
|
|
56
|
+
/**
|
|
57
|
+
* Get persistent cache directory for traces
|
|
58
|
+
* Uses ~/.sentience/traces/pending/ (survives process crashes)
|
|
59
|
+
*/
|
|
60
|
+
function getPersistentCacheDir() {
|
|
61
|
+
const homeDir = os.homedir();
|
|
62
|
+
const cacheDir = path.join(homeDir, '.sentience', 'traces', 'pending');
|
|
63
|
+
// Create directory if it doesn't exist
|
|
64
|
+
if (!fs.existsSync(cacheDir)) {
|
|
65
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
return cacheDir;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* CloudTraceSink writes trace events to a local temp file,
|
|
71
|
+
* then uploads the complete trace to cloud storage on close()
|
|
72
|
+
*
|
|
73
|
+
* Architecture:
|
|
74
|
+
* 1. **Local Buffer**: Writes to temp file (zero latency, non-blocking)
|
|
75
|
+
* 2. **Pre-signed URL**: Uses secure pre-signed PUT URL from backend API
|
|
76
|
+
* 3. **Batch Upload**: Uploads complete file on close() or at intervals
|
|
77
|
+
* 4. **Zero Credential Exposure**: Never embeds cloud credentials in SDK
|
|
78
|
+
*
|
|
79
|
+
* This design ensures:
|
|
80
|
+
* - Fast agent performance (microseconds per emit, not milliseconds)
|
|
81
|
+
* - Security (credentials stay on backend)
|
|
82
|
+
* - Reliability (network issues don't crash the agent)
|
|
83
|
+
*
|
|
84
|
+
* Example:
|
|
85
|
+
* const sink = new CloudTraceSink(uploadUrl);
|
|
86
|
+
* const tracer = new Tracer(runId, sink);
|
|
87
|
+
* tracer.emitRunStart('SentienceAgent');
|
|
88
|
+
* await tracer.close(); // Uploads to cloud
|
|
89
|
+
*/
|
|
90
|
+
class CloudTraceSink extends sink_1.TraceSink {
|
|
91
|
+
/**
|
|
92
|
+
* Create a new CloudTraceSink
|
|
93
|
+
*
|
|
94
|
+
* @param uploadUrl - Pre-signed PUT URL from Sentience API
|
|
95
|
+
* @param runId - Run ID for persistent cache naming
|
|
96
|
+
* @param apiKey - Sentience API key for calling /v1/traces/complete
|
|
97
|
+
* @param apiUrl - Sentience API base URL (default: https://api.sentienceapi.com)
|
|
98
|
+
* @param logger - Optional logger instance for logging file sizes and errors
|
|
99
|
+
*/
|
|
100
|
+
constructor(uploadUrl, runId, apiKey, apiUrl, logger) {
|
|
101
|
+
super();
|
|
102
|
+
this.writeStream = null;
|
|
103
|
+
this.closed = false;
|
|
104
|
+
// File size tracking
|
|
105
|
+
this.traceFileSizeBytes = 0;
|
|
106
|
+
this.screenshotTotalSizeBytes = 0;
|
|
107
|
+
this.screenshotCount = 0; // Track number of screenshots extracted
|
|
108
|
+
this.indexFileSizeBytes = 0; // Track index file size
|
|
109
|
+
// Upload success flag
|
|
110
|
+
this.uploadSuccessful = false;
|
|
111
|
+
this.uploadUrl = uploadUrl;
|
|
112
|
+
this.runId = runId || `trace-${Date.now()}`;
|
|
113
|
+
this.apiKey = apiKey;
|
|
114
|
+
this.apiUrl = apiUrl || 'https://api.sentienceapi.com';
|
|
115
|
+
this.logger = logger;
|
|
116
|
+
// PRODUCTION FIX: Use persistent cache directory instead of /tmp
|
|
117
|
+
// This ensures traces survive process crashes!
|
|
118
|
+
const cacheDir = getPersistentCacheDir();
|
|
119
|
+
this.tempFilePath = path.join(cacheDir, `${this.runId}.jsonl`);
|
|
120
|
+
try {
|
|
121
|
+
// Open file in append mode
|
|
122
|
+
this.writeStream = fs.createWriteStream(this.tempFilePath, {
|
|
123
|
+
flags: 'a',
|
|
124
|
+
encoding: 'utf-8',
|
|
125
|
+
autoClose: true,
|
|
126
|
+
});
|
|
127
|
+
// Handle stream errors (suppress if closed)
|
|
128
|
+
this.writeStream.on('error', error => {
|
|
129
|
+
if (!this.closed) {
|
|
130
|
+
console.error('[CloudTraceSink] Stream error:', error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error('[CloudTraceSink] Failed to initialize sink:', error);
|
|
136
|
+
this.writeStream = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Emit a trace event to local temp file (fast, non-blocking)
|
|
141
|
+
*
|
|
142
|
+
* @param event - Trace event to emit
|
|
143
|
+
*/
|
|
144
|
+
emit(event) {
|
|
145
|
+
if (this.closed) {
|
|
146
|
+
throw new Error('CloudTraceSink is closed');
|
|
147
|
+
}
|
|
148
|
+
if (!this.writeStream) {
|
|
149
|
+
console.error('[CloudTraceSink] Write stream not available');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const jsonStr = JSON.stringify(event);
|
|
154
|
+
this.writeStream.write(jsonStr + '\n');
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error('[CloudTraceSink] Write error:', error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Upload data to cloud using Node's built-in https module
|
|
162
|
+
*/
|
|
163
|
+
async _uploadToCloud(data) {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const url = new url_1.URL(this.uploadUrl);
|
|
166
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
167
|
+
const options = {
|
|
168
|
+
hostname: url.hostname,
|
|
169
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
170
|
+
path: url.pathname + url.search,
|
|
171
|
+
method: 'PUT',
|
|
172
|
+
headers: {
|
|
173
|
+
'Content-Type': 'application/x-gzip',
|
|
174
|
+
'Content-Encoding': 'gzip',
|
|
175
|
+
'Content-Length': data.length,
|
|
176
|
+
},
|
|
177
|
+
timeout: 60000, // 1 minute timeout
|
|
178
|
+
};
|
|
179
|
+
const req = protocol.request(options, res => {
|
|
180
|
+
// Consume response data (even if we don't use it)
|
|
181
|
+
res.on('data', () => { });
|
|
182
|
+
res.on('end', () => {
|
|
183
|
+
resolve(res.statusCode || 500);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
req.on('error', error => {
|
|
187
|
+
reject(error);
|
|
188
|
+
});
|
|
189
|
+
req.on('timeout', () => {
|
|
190
|
+
req.destroy();
|
|
191
|
+
reject(new Error('Upload timeout'));
|
|
192
|
+
});
|
|
193
|
+
req.write(data);
|
|
194
|
+
req.end();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Upload buffered trace to cloud via pre-signed URL
|
|
199
|
+
*
|
|
200
|
+
* @param blocking - If false, upload happens in background (default: true)
|
|
201
|
+
*
|
|
202
|
+
* PRODUCTION FIX: Non-blocking mode prevents hanging user scripts
|
|
203
|
+
* on slow uploads (Risk #2 from production hardening plan)
|
|
204
|
+
*/
|
|
205
|
+
async close(blocking = true) {
|
|
206
|
+
if (this.closed) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.closed = true;
|
|
210
|
+
// Non-blocking mode: fire-and-forget background upload
|
|
211
|
+
if (!blocking) {
|
|
212
|
+
// Close the write stream synchronously
|
|
213
|
+
if (this.writeStream && !this.writeStream.destroyed) {
|
|
214
|
+
this.writeStream.end();
|
|
215
|
+
}
|
|
216
|
+
// Upload in background (don't await)
|
|
217
|
+
this._doUpload().catch(error => {
|
|
218
|
+
console.error(`❌ [Sentience] Background upload failed: ${error.message}`);
|
|
219
|
+
console.error(` Local trace preserved at: ${this.tempFilePath}`);
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Blocking mode: wait for upload to complete
|
|
224
|
+
await this._doUpload();
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Internal upload logic (called by both blocking and non-blocking close)
|
|
228
|
+
*/
|
|
229
|
+
async _doUpload() {
|
|
230
|
+
try {
|
|
231
|
+
// 1. Close write stream
|
|
232
|
+
if (this.writeStream && !this.writeStream.destroyed) {
|
|
233
|
+
const stream = this.writeStream;
|
|
234
|
+
stream.removeAllListeners('error');
|
|
235
|
+
await new Promise(resolve => {
|
|
236
|
+
stream.end(() => {
|
|
237
|
+
resolve();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// 2. Generate index after closing file
|
|
242
|
+
this.generateIndex();
|
|
243
|
+
// 2. Check trace file exists
|
|
244
|
+
try {
|
|
245
|
+
await fs_1.promises.access(this.tempFilePath);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
console.warn('[CloudTraceSink] Temp file does not exist, skipping upload');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// 3. Extract screenshots from trace events
|
|
252
|
+
const screenshots = await this._extractScreenshotsFromTrace();
|
|
253
|
+
this.screenshotCount = screenshots.size;
|
|
254
|
+
// 4. Upload screenshots separately
|
|
255
|
+
if (screenshots.size > 0) {
|
|
256
|
+
await this._uploadScreenshots(screenshots);
|
|
257
|
+
}
|
|
258
|
+
// 5. Create cleaned trace file (without screenshot_base64)
|
|
259
|
+
const cleanedTracePath = this.tempFilePath.replace('.jsonl', '.cleaned.jsonl');
|
|
260
|
+
await this._createCleanedTrace(cleanedTracePath);
|
|
261
|
+
// 6. Read and compress cleaned trace
|
|
262
|
+
const traceData = await fs_1.promises.readFile(cleanedTracePath);
|
|
263
|
+
const compressedData = zlib.gzipSync(traceData);
|
|
264
|
+
// Measure trace file size
|
|
265
|
+
this.traceFileSizeBytes = compressedData.length;
|
|
266
|
+
// Log file sizes if logger is provided
|
|
267
|
+
if (this.logger) {
|
|
268
|
+
this.logger.info(`Trace file size: ${(this.traceFileSizeBytes / 1024 / 1024).toFixed(2)} MB`);
|
|
269
|
+
this.logger.info(`Screenshot total: ${(this.screenshotTotalSizeBytes / 1024 / 1024).toFixed(2)} MB`);
|
|
270
|
+
}
|
|
271
|
+
// 7. Upload cleaned trace to cloud
|
|
272
|
+
if (this.logger) {
|
|
273
|
+
this.logger.info(`Uploading trace to cloud (${compressedData.length} bytes)`);
|
|
274
|
+
}
|
|
275
|
+
const statusCode = await this._uploadToCloud(compressedData);
|
|
276
|
+
if (statusCode === 200) {
|
|
277
|
+
this.uploadSuccessful = true;
|
|
278
|
+
if (this.logger) {
|
|
279
|
+
this.logger.info('Trace uploaded successfully');
|
|
280
|
+
}
|
|
281
|
+
// Upload trace index file
|
|
282
|
+
await this._uploadIndex();
|
|
283
|
+
// Call /v1/traces/complete to report file sizes
|
|
284
|
+
await this._completeTrace();
|
|
285
|
+
// 8. Delete files on success
|
|
286
|
+
await this._cleanupFiles();
|
|
287
|
+
// Clean up temporary cleaned trace file
|
|
288
|
+
try {
|
|
289
|
+
await fs_1.promises.unlink(cleanedTracePath);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Ignore cleanup errors
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.uploadSuccessful = false;
|
|
297
|
+
console.error(`❌ [Sentience] Upload failed: HTTP ${statusCode}`);
|
|
298
|
+
console.error(` Local trace preserved at: ${this.tempFilePath}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
console.error(`❌ [Sentience] Error uploading trace: ${error.message}`);
|
|
303
|
+
console.error(` Local trace preserved at: ${this.tempFilePath}`);
|
|
304
|
+
// Don't throw - preserve trace locally even if upload fails
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Infer final status from trace events by reading the trace file.
|
|
309
|
+
* @returns Final status: "success", "failure", "partial", or "unknown"
|
|
310
|
+
*/
|
|
311
|
+
_inferFinalStatusFromTrace() {
|
|
312
|
+
try {
|
|
313
|
+
// Read trace file to analyze events
|
|
314
|
+
const traceContent = fs.readFileSync(this.tempFilePath, 'utf-8');
|
|
315
|
+
const lines = traceContent.split('\n').filter(line => line.trim());
|
|
316
|
+
const events = [];
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
try {
|
|
319
|
+
const event = JSON.parse(line);
|
|
320
|
+
events.push(event);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (events.length === 0) {
|
|
327
|
+
return 'unknown';
|
|
328
|
+
}
|
|
329
|
+
// Check for run_end event with status
|
|
330
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
331
|
+
const event = events[i];
|
|
332
|
+
if (event.type === 'run_end') {
|
|
333
|
+
const status = event.data?.status;
|
|
334
|
+
if (status === 'success' ||
|
|
335
|
+
status === 'failure' ||
|
|
336
|
+
status === 'partial' ||
|
|
337
|
+
status === 'unknown') {
|
|
338
|
+
return status;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Infer from error events
|
|
343
|
+
const hasErrors = events.some(e => e.type === 'error');
|
|
344
|
+
if (hasErrors) {
|
|
345
|
+
// Check if there are successful steps too (partial success)
|
|
346
|
+
const stepEnds = events.filter(e => e.type === 'step_end');
|
|
347
|
+
if (stepEnds.length > 0) {
|
|
348
|
+
return 'partial';
|
|
349
|
+
}
|
|
350
|
+
return 'failure';
|
|
351
|
+
}
|
|
352
|
+
// If we have step_end events and no errors, likely success
|
|
353
|
+
const stepEnds = events.filter(e => e.type === 'step_end');
|
|
354
|
+
if (stepEnds.length > 0) {
|
|
355
|
+
return 'success';
|
|
356
|
+
}
|
|
357
|
+
return 'unknown';
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// If we can't read the trace, default to unknown
|
|
361
|
+
return 'unknown';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Extract execution statistics from trace file.
|
|
366
|
+
* @returns Trace statistics for /v1/traces/complete
|
|
367
|
+
*/
|
|
368
|
+
_extractStatsFromTrace() {
|
|
369
|
+
try {
|
|
370
|
+
// Read trace file to extract stats
|
|
371
|
+
const traceContent = fs.readFileSync(this.tempFilePath, 'utf-8');
|
|
372
|
+
const lines = traceContent.split('\n').filter(line => line.trim());
|
|
373
|
+
const events = [];
|
|
374
|
+
for (const line of lines) {
|
|
375
|
+
try {
|
|
376
|
+
const event = JSON.parse(line);
|
|
377
|
+
events.push(event);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (events.length === 0) {
|
|
384
|
+
return {
|
|
385
|
+
total_steps: 0,
|
|
386
|
+
total_events: 0,
|
|
387
|
+
duration_ms: null,
|
|
388
|
+
final_status: 'unknown',
|
|
389
|
+
started_at: null,
|
|
390
|
+
ended_at: null,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// Find run_start and run_end events
|
|
394
|
+
const runStart = events.find(e => e.type === 'run_start');
|
|
395
|
+
const runEnd = events.find(e => e.type === 'run_end');
|
|
396
|
+
// Extract timestamps
|
|
397
|
+
const startedAt = runStart?.ts || null;
|
|
398
|
+
const endedAt = runEnd?.ts || null;
|
|
399
|
+
// Calculate duration
|
|
400
|
+
let durationMs = null;
|
|
401
|
+
if (startedAt && endedAt) {
|
|
402
|
+
try {
|
|
403
|
+
const startDt = new Date(startedAt);
|
|
404
|
+
const endDt = new Date(endedAt);
|
|
405
|
+
durationMs = endDt.getTime() - startDt.getTime();
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// Ignore parse errors
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Count steps (from step_start events, only first attempt)
|
|
412
|
+
const stepIndices = new Set();
|
|
413
|
+
for (const event of events) {
|
|
414
|
+
if (event.type === 'step_start') {
|
|
415
|
+
const stepIndex = event.data?.step_index;
|
|
416
|
+
if (stepIndex !== undefined) {
|
|
417
|
+
stepIndices.add(stepIndex);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
let totalSteps = stepIndices.size;
|
|
422
|
+
// If run_end has steps count, use that (more accurate)
|
|
423
|
+
if (runEnd) {
|
|
424
|
+
const stepsFromEnd = runEnd.data?.steps;
|
|
425
|
+
if (stepsFromEnd !== undefined) {
|
|
426
|
+
totalSteps = Math.max(totalSteps, stepsFromEnd);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Count total events
|
|
430
|
+
const totalEvents = events.length;
|
|
431
|
+
// Infer final status
|
|
432
|
+
const finalStatus = this._inferFinalStatusFromTrace();
|
|
433
|
+
return {
|
|
434
|
+
total_steps: totalSteps,
|
|
435
|
+
total_events: totalEvents,
|
|
436
|
+
duration_ms: durationMs,
|
|
437
|
+
final_status: finalStatus,
|
|
438
|
+
started_at: startedAt,
|
|
439
|
+
ended_at: endedAt,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
this.logger?.warn(`Error extracting stats from trace: ${error.message}`);
|
|
444
|
+
return {
|
|
445
|
+
total_steps: 0,
|
|
446
|
+
total_events: 0,
|
|
447
|
+
duration_ms: null,
|
|
448
|
+
final_status: 'unknown',
|
|
449
|
+
started_at: null,
|
|
450
|
+
ended_at: null,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Call /v1/traces/complete to report file sizes and stats to gateway.
|
|
456
|
+
*
|
|
457
|
+
* This is a best-effort call - failures are logged but don't affect upload success.
|
|
458
|
+
*/
|
|
459
|
+
async _completeTrace() {
|
|
460
|
+
if (!this.apiKey) {
|
|
461
|
+
// No API key - skip complete call
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
return new Promise(resolve => {
|
|
465
|
+
const url = new url_1.URL(`${this.apiUrl}/v1/traces/complete`);
|
|
466
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
467
|
+
// Extract stats from trace file
|
|
468
|
+
const stats = this._extractStatsFromTrace();
|
|
469
|
+
// Add file size fields
|
|
470
|
+
const completeStats = {
|
|
471
|
+
...stats,
|
|
472
|
+
trace_file_size_bytes: this.traceFileSizeBytes,
|
|
473
|
+
screenshot_total_size_bytes: this.screenshotTotalSizeBytes,
|
|
474
|
+
screenshot_count: this.screenshotCount,
|
|
475
|
+
index_file_size_bytes: this.indexFileSizeBytes,
|
|
476
|
+
};
|
|
477
|
+
const body = JSON.stringify({
|
|
478
|
+
run_id: this.runId,
|
|
479
|
+
stats: completeStats,
|
|
480
|
+
});
|
|
481
|
+
const options = {
|
|
482
|
+
hostname: url.hostname,
|
|
483
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
484
|
+
path: url.pathname + url.search,
|
|
485
|
+
method: 'POST',
|
|
486
|
+
headers: {
|
|
487
|
+
'Content-Type': 'application/json',
|
|
488
|
+
'Content-Length': Buffer.byteLength(body),
|
|
489
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
490
|
+
},
|
|
491
|
+
timeout: 10000, // 10 second timeout
|
|
492
|
+
};
|
|
493
|
+
const req = protocol.request(options, res => {
|
|
494
|
+
// Consume response data
|
|
495
|
+
res.on('data', () => { });
|
|
496
|
+
res.on('end', () => {
|
|
497
|
+
if (res.statusCode === 200) {
|
|
498
|
+
this.logger?.info('Trace completion reported to gateway');
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
this.logger?.warn(`Failed to report trace completion: HTTP ${res.statusCode}`);
|
|
502
|
+
}
|
|
503
|
+
resolve();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
req.on('error', error => {
|
|
507
|
+
// Best-effort - log but don't fail
|
|
508
|
+
this.logger?.warn(`Error reporting trace completion: ${error.message}`);
|
|
509
|
+
resolve();
|
|
510
|
+
});
|
|
511
|
+
req.on('timeout', () => {
|
|
512
|
+
req.destroy();
|
|
513
|
+
this.logger?.warn('Trace completion request timeout');
|
|
514
|
+
resolve();
|
|
515
|
+
});
|
|
516
|
+
req.write(body);
|
|
517
|
+
req.end();
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Generate trace index file (automatic on close)
|
|
522
|
+
*/
|
|
523
|
+
generateIndex() {
|
|
524
|
+
try {
|
|
525
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
526
|
+
const { writeTraceIndex } = require('./indexer');
|
|
527
|
+
// Use frontend format to ensure 'step' field is present (1-based)
|
|
528
|
+
// Frontend derives sequence from step.step - 1, so step must be valid
|
|
529
|
+
const indexPath = this.tempFilePath.replace('.jsonl', '.index.json');
|
|
530
|
+
writeTraceIndex(this.tempFilePath, indexPath, true);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
// Non-fatal: log but don't crash
|
|
534
|
+
this.logger?.warn(`Failed to generate trace index: ${error.message}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Upload trace index file to cloud storage.
|
|
539
|
+
*
|
|
540
|
+
* Called after successful trace upload to provide fast timeline rendering.
|
|
541
|
+
* The index file enables O(1) step lookups without parsing the entire trace.
|
|
542
|
+
*/
|
|
543
|
+
async _uploadIndex() {
|
|
544
|
+
// Construct index file path (same as trace file with .index.json extension)
|
|
545
|
+
const indexPath = this.tempFilePath.replace('.jsonl', '.index.json');
|
|
546
|
+
try {
|
|
547
|
+
// Check if index file exists
|
|
548
|
+
await fs_1.promises.access(indexPath);
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
this.logger?.warn('Index file not found, skipping index upload');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
// Request index upload URL from API
|
|
556
|
+
if (!this.apiKey) {
|
|
557
|
+
this.logger?.info('No API key provided, skipping index upload');
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const uploadUrlResponse = await this._requestIndexUploadUrl();
|
|
561
|
+
if (!uploadUrlResponse) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
// Read index file and update trace_file.path to cloud storage path
|
|
565
|
+
const indexContent = await fs_1.promises.readFile(indexPath, 'utf-8');
|
|
566
|
+
const indexJson = JSON.parse(indexContent);
|
|
567
|
+
// Extract cloud storage path from trace upload URL
|
|
568
|
+
// uploadUrl format: https://...digitaloceanspaces.com/traces/{run_id}.jsonl.gz
|
|
569
|
+
// Extract path: traces/{run_id}.jsonl.gz
|
|
570
|
+
try {
|
|
571
|
+
const parsedUrl = new url_1.URL(this.uploadUrl);
|
|
572
|
+
// Extract path after domain (e.g., /traces/run-123.jsonl.gz -> traces/run-123.jsonl.gz)
|
|
573
|
+
const cloudTracePath = parsedUrl.pathname.startsWith('/')
|
|
574
|
+
? parsedUrl.pathname.substring(1)
|
|
575
|
+
: parsedUrl.pathname;
|
|
576
|
+
// Update trace_file.path in index
|
|
577
|
+
if (indexJson.trace_file && typeof indexJson.trace_file === 'object') {
|
|
578
|
+
indexJson.trace_file.path = cloudTracePath;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
this.logger?.warn(`Failed to extract cloud path from upload URL: ${error.message}`);
|
|
583
|
+
}
|
|
584
|
+
// Serialize updated index to JSON
|
|
585
|
+
const updatedIndexData = Buffer.from(JSON.stringify(indexJson, null, 2), 'utf-8');
|
|
586
|
+
const compressedIndex = zlib.gzipSync(updatedIndexData);
|
|
587
|
+
const indexSize = compressedIndex.length;
|
|
588
|
+
this.indexFileSizeBytes = indexSize; // Track index file size
|
|
589
|
+
this.logger?.info(`Index file size: ${(indexSize / 1024).toFixed(2)} KB`);
|
|
590
|
+
if (this.logger) {
|
|
591
|
+
this.logger.info(`Uploading trace index (${indexSize} bytes)`);
|
|
592
|
+
}
|
|
593
|
+
// Upload index to cloud storage
|
|
594
|
+
const statusCode = await this._uploadIndexToCloud(uploadUrlResponse, compressedIndex);
|
|
595
|
+
if (statusCode === 200) {
|
|
596
|
+
if (this.logger) {
|
|
597
|
+
this.logger.info('Trace index uploaded successfully');
|
|
598
|
+
}
|
|
599
|
+
// Delete local index file after successful upload
|
|
600
|
+
try {
|
|
601
|
+
await fs_1.promises.unlink(indexPath);
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// Ignore cleanup errors
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
this.logger?.warn(`Index upload failed: HTTP ${statusCode}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
// Non-fatal: log but don't crash
|
|
613
|
+
this.logger?.warn(`Error uploading trace index: ${error.message}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Request index upload URL from Sentience API
|
|
618
|
+
*/
|
|
619
|
+
async _requestIndexUploadUrl() {
|
|
620
|
+
return new Promise(resolve => {
|
|
621
|
+
const url = new url_1.URL(`${this.apiUrl}/v1/traces/index_upload`);
|
|
622
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
623
|
+
const body = JSON.stringify({ run_id: this.runId });
|
|
624
|
+
const options = {
|
|
625
|
+
hostname: url.hostname,
|
|
626
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
627
|
+
path: url.pathname + url.search,
|
|
628
|
+
method: 'POST',
|
|
629
|
+
headers: {
|
|
630
|
+
'Content-Type': 'application/json',
|
|
631
|
+
'Content-Length': Buffer.byteLength(body),
|
|
632
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
633
|
+
},
|
|
634
|
+
timeout: 10000,
|
|
635
|
+
};
|
|
636
|
+
const req = protocol.request(options, res => {
|
|
637
|
+
let data = '';
|
|
638
|
+
res.on('data', chunk => {
|
|
639
|
+
data += chunk;
|
|
640
|
+
});
|
|
641
|
+
res.on('end', () => {
|
|
642
|
+
if (res.statusCode === 200) {
|
|
643
|
+
try {
|
|
644
|
+
const response = JSON.parse(data);
|
|
645
|
+
resolve(response.upload_url || null);
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
this.logger?.warn('Failed to parse index upload URL response');
|
|
649
|
+
resolve(null);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
this.logger?.warn(`Failed to get index upload URL: HTTP ${res.statusCode}`);
|
|
654
|
+
resolve(null);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
req.on('error', error => {
|
|
659
|
+
this.logger?.warn(`Error requesting index upload URL: ${error.message}`);
|
|
660
|
+
resolve(null);
|
|
661
|
+
});
|
|
662
|
+
req.on('timeout', () => {
|
|
663
|
+
req.destroy();
|
|
664
|
+
this.logger?.warn('Index upload URL request timeout');
|
|
665
|
+
resolve(null);
|
|
666
|
+
});
|
|
667
|
+
req.write(body);
|
|
668
|
+
req.end();
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Upload index data to cloud using pre-signed URL
|
|
673
|
+
*/
|
|
674
|
+
async _uploadIndexToCloud(uploadUrl, data) {
|
|
675
|
+
return new Promise((resolve, reject) => {
|
|
676
|
+
const url = new url_1.URL(uploadUrl);
|
|
677
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
678
|
+
const options = {
|
|
679
|
+
hostname: url.hostname,
|
|
680
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
681
|
+
path: url.pathname + url.search,
|
|
682
|
+
method: 'PUT',
|
|
683
|
+
headers: {
|
|
684
|
+
'Content-Type': 'application/json',
|
|
685
|
+
'Content-Encoding': 'gzip',
|
|
686
|
+
'Content-Length': data.length,
|
|
687
|
+
},
|
|
688
|
+
timeout: 30000, // 30 second timeout
|
|
689
|
+
};
|
|
690
|
+
const req = protocol.request(options, res => {
|
|
691
|
+
res.on('data', () => { });
|
|
692
|
+
res.on('end', () => {
|
|
693
|
+
resolve(res.statusCode || 500);
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
req.on('error', error => {
|
|
697
|
+
reject(error);
|
|
698
|
+
});
|
|
699
|
+
req.on('timeout', () => {
|
|
700
|
+
req.destroy();
|
|
701
|
+
reject(new Error('Index upload timeout'));
|
|
702
|
+
});
|
|
703
|
+
req.write(data);
|
|
704
|
+
req.end();
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Normalize screenshot data by extracting base64 from data URL if needed.
|
|
709
|
+
*
|
|
710
|
+
* Handles both formats:
|
|
711
|
+
* - Data URL: "data:image/jpeg;base64,/9j/4AAQ..."
|
|
712
|
+
* - Pure base64: "/9j/4AAQ..."
|
|
713
|
+
*
|
|
714
|
+
* @param screenshotRaw - Raw screenshot data (data URL or base64)
|
|
715
|
+
* @param defaultFormat - Default format if not detected from data URL
|
|
716
|
+
* @returns Tuple of [base64String, formatString]
|
|
717
|
+
*/
|
|
718
|
+
_normalizeScreenshotData(screenshotRaw, defaultFormat = 'jpeg') {
|
|
719
|
+
if (!screenshotRaw) {
|
|
720
|
+
return ['', defaultFormat];
|
|
721
|
+
}
|
|
722
|
+
// Check if it's a data URL
|
|
723
|
+
if (screenshotRaw.startsWith('data:image')) {
|
|
724
|
+
// Extract format from "data:image/jpeg;base64,..." or "data:image/png;base64,..."
|
|
725
|
+
try {
|
|
726
|
+
// Split on comma to get the base64 part
|
|
727
|
+
if (screenshotRaw.includes(',')) {
|
|
728
|
+
const [header, base64Data] = screenshotRaw.split(',', 2);
|
|
729
|
+
// Extract format from header: "data:image/jpeg;base64"
|
|
730
|
+
if (header.includes('/') && header.includes(';')) {
|
|
731
|
+
const formatPart = header.split('/')[1]?.split(';')[0];
|
|
732
|
+
if (formatPart === 'jpeg' || formatPart === 'jpg') {
|
|
733
|
+
return [base64Data, 'jpeg'];
|
|
734
|
+
}
|
|
735
|
+
else if (formatPart === 'png') {
|
|
736
|
+
return [base64Data, 'png'];
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return [base64Data, defaultFormat];
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
// Malformed data URL - return as-is with warning
|
|
743
|
+
this.logger?.warn('Malformed data URL in screenshot_base64 (missing comma)');
|
|
744
|
+
return [screenshotRaw, defaultFormat];
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
this.logger?.warn(`Error parsing screenshot data URL: ${error.message}`);
|
|
749
|
+
return [screenshotRaw, defaultFormat];
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Already pure base64
|
|
753
|
+
return [screenshotRaw, defaultFormat];
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Extract screenshots from trace events.
|
|
757
|
+
*
|
|
758
|
+
* @returns Map of sequence number to screenshot data
|
|
759
|
+
*/
|
|
760
|
+
async _extractScreenshotsFromTrace() {
|
|
761
|
+
const screenshots = new Map();
|
|
762
|
+
let sequence = 0;
|
|
763
|
+
try {
|
|
764
|
+
const traceContent = await fs_1.promises.readFile(this.tempFilePath, 'utf-8');
|
|
765
|
+
const lines = traceContent.split('\n');
|
|
766
|
+
for (const line of lines) {
|
|
767
|
+
if (!line.trim()) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
const event = JSON.parse(line);
|
|
772
|
+
// Check if this is a snapshot event with screenshot
|
|
773
|
+
if (event.type === 'snapshot') {
|
|
774
|
+
const data = event.data || {};
|
|
775
|
+
const screenshotRaw = data.screenshot_base64;
|
|
776
|
+
if (screenshotRaw) {
|
|
777
|
+
// Normalize: extract base64 from data URL if needed
|
|
778
|
+
// Handles both "data:image/jpeg;base64,..." and pure base64
|
|
779
|
+
const [screenshotBase64, screenshotFormat] = this._normalizeScreenshotData(screenshotRaw, data.screenshot_format || 'jpeg');
|
|
780
|
+
if (screenshotBase64) {
|
|
781
|
+
sequence += 1;
|
|
782
|
+
screenshots.set(sequence, {
|
|
783
|
+
base64: screenshotBase64,
|
|
784
|
+
format: screenshotFormat,
|
|
785
|
+
stepId: event.step_id,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
// Skip invalid JSON lines
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
this.logger?.error(`Error extracting screenshots: ${error.message}`);
|
|
799
|
+
}
|
|
800
|
+
return screenshots;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Create trace file without screenshot_base64 fields.
|
|
804
|
+
*
|
|
805
|
+
* @param outputPath - Path to write cleaned trace file
|
|
806
|
+
*/
|
|
807
|
+
async _createCleanedTrace(outputPath) {
|
|
808
|
+
try {
|
|
809
|
+
const traceContent = await fs_1.promises.readFile(this.tempFilePath, 'utf-8');
|
|
810
|
+
const lines = traceContent.split('\n');
|
|
811
|
+
const cleanedLines = [];
|
|
812
|
+
for (const line of lines) {
|
|
813
|
+
if (!line.trim()) {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
const event = JSON.parse(line);
|
|
818
|
+
// Remove screenshot_base64 from snapshot events
|
|
819
|
+
if (event.type === 'snapshot' && event.data) {
|
|
820
|
+
const cleanedData = {};
|
|
821
|
+
for (const [key, value] of Object.entries(event.data)) {
|
|
822
|
+
if (key !== 'screenshot_base64' && key !== 'screenshot_format') {
|
|
823
|
+
cleanedData[key] = value;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
event.data = cleanedData;
|
|
827
|
+
}
|
|
828
|
+
cleanedLines.push(JSON.stringify(event));
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
// Skip invalid JSON lines
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
await fs_1.promises.writeFile(outputPath, cleanedLines.join('\n') + '\n', 'utf-8');
|
|
836
|
+
}
|
|
837
|
+
catch (error) {
|
|
838
|
+
this.logger?.error(`Error creating cleaned trace: ${error.message}`);
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Request pre-signed upload URLs for screenshots from gateway.
|
|
844
|
+
*
|
|
845
|
+
* @param sequences - List of screenshot sequence numbers
|
|
846
|
+
* @returns Map of sequence number to upload URL
|
|
847
|
+
*/
|
|
848
|
+
async _requestScreenshotUrls(sequences) {
|
|
849
|
+
if (!this.apiKey || sequences.length === 0) {
|
|
850
|
+
return new Map();
|
|
851
|
+
}
|
|
852
|
+
return new Promise(resolve => {
|
|
853
|
+
const url = new url_1.URL(`${this.apiUrl}/v1/screenshots/init`);
|
|
854
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
855
|
+
const body = JSON.stringify({
|
|
856
|
+
run_id: this.runId,
|
|
857
|
+
sequences,
|
|
858
|
+
});
|
|
859
|
+
const options = {
|
|
860
|
+
hostname: url.hostname,
|
|
861
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
862
|
+
path: url.pathname + url.search,
|
|
863
|
+
method: 'POST',
|
|
864
|
+
headers: {
|
|
865
|
+
'Content-Type': 'application/json',
|
|
866
|
+
'Content-Length': Buffer.byteLength(body),
|
|
867
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
868
|
+
},
|
|
869
|
+
timeout: 10000, // 10 second timeout
|
|
870
|
+
};
|
|
871
|
+
const req = protocol.request(options, res => {
|
|
872
|
+
let data = '';
|
|
873
|
+
res.on('data', chunk => {
|
|
874
|
+
data += chunk;
|
|
875
|
+
});
|
|
876
|
+
res.on('end', () => {
|
|
877
|
+
if (res.statusCode === 200) {
|
|
878
|
+
try {
|
|
879
|
+
const response = JSON.parse(data);
|
|
880
|
+
const uploadUrls = response.upload_urls || {};
|
|
881
|
+
const urlMap = new Map();
|
|
882
|
+
// Gateway returns sequences as strings in JSON, convert to int keys
|
|
883
|
+
for (const [seqStr, url] of Object.entries(uploadUrls)) {
|
|
884
|
+
urlMap.set(parseInt(seqStr, 10), url);
|
|
885
|
+
}
|
|
886
|
+
resolve(urlMap);
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
this.logger?.warn('Failed to parse screenshot upload URLs response');
|
|
890
|
+
resolve(new Map());
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
this.logger?.warn(`Failed to get screenshot URLs: HTTP ${res.statusCode}`);
|
|
895
|
+
resolve(new Map());
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
req.on('error', error => {
|
|
900
|
+
this.logger?.warn(`Error requesting screenshot URLs: ${error.message}`);
|
|
901
|
+
resolve(new Map());
|
|
902
|
+
});
|
|
903
|
+
req.on('timeout', () => {
|
|
904
|
+
req.destroy();
|
|
905
|
+
this.logger?.warn('Screenshot URLs request timeout');
|
|
906
|
+
resolve(new Map());
|
|
907
|
+
});
|
|
908
|
+
req.write(body);
|
|
909
|
+
req.end();
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Upload screenshots extracted from trace events.
|
|
914
|
+
*
|
|
915
|
+
* Steps:
|
|
916
|
+
* 1. Request pre-signed URLs from gateway (/v1/screenshots/init)
|
|
917
|
+
* 2. Decode base64 to image bytes
|
|
918
|
+
* 3. Upload screenshots in parallel (10 concurrent workers)
|
|
919
|
+
* 4. Track upload progress
|
|
920
|
+
*
|
|
921
|
+
* @param screenshots - Map of sequence to screenshot data
|
|
922
|
+
*/
|
|
923
|
+
async _uploadScreenshots(screenshots) {
|
|
924
|
+
if (screenshots.size === 0) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
// 1. Request pre-signed URLs from gateway
|
|
928
|
+
const sequences = Array.from(screenshots.keys()).sort((a, b) => a - b);
|
|
929
|
+
const uploadUrls = await this._requestScreenshotUrls(sequences);
|
|
930
|
+
if (uploadUrls.size === 0) {
|
|
931
|
+
this.logger?.warn('No screenshot upload URLs received, skipping upload. This may indicate API key permission issue, gateway error, or network problem.');
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
// 2. Upload screenshots in parallel
|
|
935
|
+
const uploadPromises = [];
|
|
936
|
+
const uploadSequences = [];
|
|
937
|
+
uploadUrls.forEach((url, seq) => {
|
|
938
|
+
const screenshotData = screenshots.get(seq);
|
|
939
|
+
if (!screenshotData) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
uploadSequences.push(seq);
|
|
943
|
+
const uploadPromise = this._uploadSingleScreenshot(seq, url, screenshotData);
|
|
944
|
+
uploadPromises.push(uploadPromise);
|
|
945
|
+
});
|
|
946
|
+
// Wait for all uploads (max 10 concurrent)
|
|
947
|
+
const results = await Promise.allSettled(uploadPromises.slice(0, 10));
|
|
948
|
+
// Process remaining uploads in batches of 10
|
|
949
|
+
for (let i = 10; i < uploadPromises.length; i += 10) {
|
|
950
|
+
const batch = uploadPromises.slice(i, i + 10);
|
|
951
|
+
const batchResults = await Promise.allSettled(batch);
|
|
952
|
+
results.push(...batchResults);
|
|
953
|
+
}
|
|
954
|
+
// Count successes and failures
|
|
955
|
+
let uploadedCount = 0;
|
|
956
|
+
const failedSequences = [];
|
|
957
|
+
for (let i = 0; i < results.length; i++) {
|
|
958
|
+
const result = results[i];
|
|
959
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
960
|
+
uploadedCount++;
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
failedSequences.push(uploadSequences[i]);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
// 3. Report results
|
|
967
|
+
const totalCount = uploadUrls.size;
|
|
968
|
+
if (uploadedCount === totalCount) {
|
|
969
|
+
const totalSizeMB = this.screenshotTotalSizeBytes / 1024 / 1024;
|
|
970
|
+
if (this.logger) {
|
|
971
|
+
this.logger.info(`All ${totalCount} screenshots uploaded successfully (total size: ${totalSizeMB.toFixed(2)} MB)`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
if (this.logger) {
|
|
976
|
+
this.logger.warn(`Uploaded ${uploadedCount}/${totalCount} screenshots. Failed sequences: ${failedSequences.length > 0 ? failedSequences.join(', ') : 'none'}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Upload a single screenshot to pre-signed URL.
|
|
982
|
+
*
|
|
983
|
+
* @param sequence - Screenshot sequence number
|
|
984
|
+
* @param uploadUrl - Pre-signed upload URL
|
|
985
|
+
* @param screenshotData - Screenshot data with base64 and format
|
|
986
|
+
* @returns True if upload successful, false otherwise
|
|
987
|
+
*/
|
|
988
|
+
async _uploadSingleScreenshot(sequence, uploadUrl, screenshotData) {
|
|
989
|
+
try {
|
|
990
|
+
// Decode base64 to image bytes
|
|
991
|
+
const imageBytes = Buffer.from(screenshotData.base64, 'base64');
|
|
992
|
+
const imageSize = imageBytes.length;
|
|
993
|
+
// Update total size
|
|
994
|
+
this.screenshotTotalSizeBytes += imageSize;
|
|
995
|
+
// Upload to pre-signed URL
|
|
996
|
+
const statusCode = await this._uploadScreenshotToCloud(uploadUrl, imageBytes, screenshotData.format);
|
|
997
|
+
if (statusCode === 200) {
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
this.logger?.warn(`Screenshot ${sequence} upload failed: HTTP ${statusCode}`);
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
this.logger?.warn(`Screenshot ${sequence} upload error: ${error.message}`);
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Upload screenshot data to cloud using pre-signed URL
|
|
1012
|
+
*/
|
|
1013
|
+
async _uploadScreenshotToCloud(uploadUrl, data, format) {
|
|
1014
|
+
return new Promise((resolve, reject) => {
|
|
1015
|
+
const url = new url_1.URL(uploadUrl);
|
|
1016
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
1017
|
+
const options = {
|
|
1018
|
+
hostname: url.hostname,
|
|
1019
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
1020
|
+
path: url.pathname + url.search,
|
|
1021
|
+
method: 'PUT',
|
|
1022
|
+
headers: {
|
|
1023
|
+
'Content-Type': `image/${format}`,
|
|
1024
|
+
'Content-Length': data.length,
|
|
1025
|
+
},
|
|
1026
|
+
timeout: 30000, // 30 second timeout per screenshot
|
|
1027
|
+
};
|
|
1028
|
+
const req = protocol.request(options, res => {
|
|
1029
|
+
res.on('data', () => { });
|
|
1030
|
+
res.on('end', () => {
|
|
1031
|
+
resolve(res.statusCode || 500);
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
req.on('error', error => {
|
|
1035
|
+
reject(error);
|
|
1036
|
+
});
|
|
1037
|
+
req.on('timeout', () => {
|
|
1038
|
+
req.destroy();
|
|
1039
|
+
reject(new Error('Screenshot upload timeout'));
|
|
1040
|
+
});
|
|
1041
|
+
req.write(data);
|
|
1042
|
+
req.end();
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Delete local files after successful upload.
|
|
1047
|
+
*/
|
|
1048
|
+
async _cleanupFiles() {
|
|
1049
|
+
// Delete trace file
|
|
1050
|
+
try {
|
|
1051
|
+
if (fs.existsSync(this.tempFilePath)) {
|
|
1052
|
+
await fs_1.promises.unlink(this.tempFilePath);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
catch {
|
|
1056
|
+
// Ignore cleanup errors
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Get unique identifier for this sink
|
|
1061
|
+
*/
|
|
1062
|
+
getSinkType() {
|
|
1063
|
+
return `CloudTraceSink(${this.uploadUrl.substring(0, 50)}...)`;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
exports.CloudTraceSink = CloudTraceSink;
|
|
1067
|
+
//# sourceMappingURL=cloud-sink.js.map
|