@dropout-ai/runtime 0.3.4 â 0.3.6
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/package.json +1 -1
- package/src/index.js +122 -50
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dropout-ai/runtime
|
|
3
|
-
* Universal AI Interaction Capture for Node.js
|
|
3
|
+
* Universal AI Interaction Capture for Node.js
|
|
4
4
|
* Patches: fetch, http.request, https.request
|
|
5
|
-
*
|
|
5
|
+
* Behavior: Splits interactions into separate User/Assistant rows for full analytics.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const https = require('https');
|
|
@@ -12,7 +12,7 @@ const crypto = require('crypto');
|
|
|
12
12
|
// --- DEFAULT CONFIGURATION ---
|
|
13
13
|
const SUPABASE_FUNCTION_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
|
|
14
14
|
|
|
15
|
-
// Known AI Domains
|
|
15
|
+
// Known AI Domains
|
|
16
16
|
const KNOWN_AI_DOMAINS = [
|
|
17
17
|
'api.openai.com', 'api.anthropic.com', 'generativelanguage.googleapis.com',
|
|
18
18
|
'aiplatform.googleapis.com', 'api.groq.com', 'api.mistral.ai', 'api.cohere.ai'
|
|
@@ -20,38 +20,51 @@ const KNOWN_AI_DOMAINS = [
|
|
|
20
20
|
|
|
21
21
|
class Dropout {
|
|
22
22
|
constructor(config = {}) {
|
|
23
|
+
// đ¨ BROWSER GUARD đ¨
|
|
24
|
+
if (typeof window !== 'undefined') {
|
|
25
|
+
console.warn("[Dropout] â ī¸ Initialization Skipped: This SDK is for Node.js/Server environments only.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
// 1. Validation
|
|
24
30
|
if (!config.apiKey || !config.projectId) {
|
|
25
31
|
console.warn("[Dropout] â ī¸ Initialization Skipped: Missing apiKey or projectId.");
|
|
26
32
|
return;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
// 2. Config Setup
|
|
35
|
+
// 2. Config & State Setup
|
|
30
36
|
this.config = config;
|
|
31
37
|
this.projectId = config.projectId;
|
|
32
38
|
this.apiKey = config.apiKey;
|
|
33
39
|
this.debug = config.debug || false;
|
|
34
40
|
this.privacy = config.privacy || 'full';
|
|
35
41
|
this.captureEndpoint = SUPABASE_FUNCTION_URL;
|
|
42
|
+
this.maxOutputBytes = 32768;
|
|
43
|
+
|
|
44
|
+
// 3. Conversation State (Matches your snippet)
|
|
45
|
+
this.turnIndex = 0;
|
|
46
|
+
this.lastTurnConfirmed = true;
|
|
47
|
+
this.lastPromptHash = null;
|
|
48
|
+
this.lastResponseHash = null;
|
|
36
49
|
|
|
37
|
-
//
|
|
50
|
+
// 4. Singleton Guard
|
|
38
51
|
if (global.__dropout_initialized__) {
|
|
39
52
|
if (this.debug) console.log("[Dropout] âšī¸ Already initialized. Skipping patch.");
|
|
40
53
|
return;
|
|
41
54
|
}
|
|
42
55
|
global.__dropout_initialized__ = true;
|
|
43
56
|
|
|
44
|
-
//
|
|
57
|
+
// 5. Initialize Identity
|
|
45
58
|
if (!global.__dropout_session_id__) {
|
|
46
59
|
global.__dropout_session_id__ = this.generateSessionId();
|
|
47
60
|
}
|
|
48
61
|
|
|
49
|
-
//
|
|
62
|
+
// 6. Start
|
|
50
63
|
if (this.debug) console.log(`[Dropout] đĸ Initialized for Project: ${this.projectId}`);
|
|
51
64
|
|
|
52
|
-
// Bind methods
|
|
65
|
+
// Bind methods
|
|
53
66
|
this.log = this.log.bind(this);
|
|
54
|
-
this.
|
|
67
|
+
this.emit = this.emit.bind(this);
|
|
55
68
|
this.patchNetwork();
|
|
56
69
|
}
|
|
57
70
|
|
|
@@ -94,6 +107,8 @@ class Dropout {
|
|
|
94
107
|
try {
|
|
95
108
|
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
|
|
96
109
|
if (parsed.model) model = parsed.model;
|
|
110
|
+
// Simple heuristic fallback if model is missing but structure looks AI-ish
|
|
111
|
+
if (provider === 'custom' && (parsed.messages || parsed.prompt)) provider = 'heuristic';
|
|
97
112
|
} catch (e) { }
|
|
98
113
|
}
|
|
99
114
|
return { provider, model };
|
|
@@ -101,13 +116,10 @@ class Dropout {
|
|
|
101
116
|
|
|
102
117
|
isAiRequest(url, bodyString) {
|
|
103
118
|
if (!url) return false;
|
|
104
|
-
// Guard: Infinite Loop Prevention
|
|
105
119
|
if (url.includes(this.captureEndpoint)) return false;
|
|
106
120
|
|
|
107
|
-
// 1. Check Known Domains
|
|
108
121
|
if (KNOWN_AI_DOMAINS.some(d => url.includes(d))) return true;
|
|
109
122
|
|
|
110
|
-
// 2. Heuristic Check (for custom endpoints)
|
|
111
123
|
if (bodyString) {
|
|
112
124
|
return (bodyString.includes('"model"') || bodyString.includes('"messages"')) &&
|
|
113
125
|
(bodyString.includes('"user"') || bodyString.includes('"prompt"'));
|
|
@@ -117,22 +129,32 @@ class Dropout {
|
|
|
117
129
|
|
|
118
130
|
// --- EMITTER ---
|
|
119
131
|
emit(payload) {
|
|
120
|
-
// 1. Guard
|
|
121
132
|
if (!this.apiKey || !this.projectId) return;
|
|
122
133
|
|
|
123
|
-
//
|
|
134
|
+
// Construct Final Payload matching your Inbox Table
|
|
124
135
|
const finalPayload = {
|
|
125
|
-
...payload,
|
|
126
136
|
project_id: this.projectId,
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
session_id: payload.session_id,
|
|
138
|
+
turn_index: payload.turn_index,
|
|
139
|
+
turn_role: payload.turn_role,
|
|
140
|
+
|
|
141
|
+
// Data Columns
|
|
142
|
+
provider: payload.provider,
|
|
143
|
+
model: payload.model,
|
|
144
|
+
latency_ms: payload.latency_ms || null,
|
|
145
|
+
|
|
146
|
+
// Content Columns (Both naming conventions)
|
|
147
|
+
content: payload.content,
|
|
148
|
+
//content_blob: payload.content,
|
|
149
|
+
content_hash: payload.content_hash,
|
|
150
|
+
|
|
151
|
+
// Metadata
|
|
152
|
+
metadata_flags: payload.metadata_flags,
|
|
129
153
|
received_at: new Date().toISOString()
|
|
130
154
|
};
|
|
131
155
|
|
|
132
|
-
this.log(`đ Sending Capture
|
|
156
|
+
this.log(`đ Sending Capture [${payload.turn_role}]`);
|
|
133
157
|
|
|
134
|
-
// 3. Fire and Forget using HTTPS (Bypassing our own patches via pure request)
|
|
135
|
-
// We use a clean request to avoid triggering our own hooks if we used global.fetch
|
|
136
158
|
const req = https.request(this.captureEndpoint, {
|
|
137
159
|
method: 'POST',
|
|
138
160
|
headers: {
|
|
@@ -148,7 +170,7 @@ class Dropout {
|
|
|
148
170
|
|
|
149
171
|
// --- CORE PATCHING ---
|
|
150
172
|
patchNetwork() {
|
|
151
|
-
const _this = this;
|
|
173
|
+
const _this = this;
|
|
152
174
|
|
|
153
175
|
// --- A. PATCH FETCH ---
|
|
154
176
|
if (global.fetch) {
|
|
@@ -156,13 +178,12 @@ class Dropout {
|
|
|
156
178
|
global.fetch = async function (input, init) {
|
|
157
179
|
const url = typeof input === 'string' ? input : input?.url;
|
|
158
180
|
|
|
159
|
-
// Safe Body Check
|
|
160
181
|
let bodyStr = "";
|
|
161
182
|
if (init && init.body) {
|
|
162
183
|
try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
163
184
|
}
|
|
164
185
|
|
|
165
|
-
// Guard
|
|
186
|
+
// 1. Guard
|
|
166
187
|
if (!_this.isAiRequest(url, bodyStr)) {
|
|
167
188
|
return originalFetch.apply(this, arguments);
|
|
168
189
|
}
|
|
@@ -170,19 +191,36 @@ class Dropout {
|
|
|
170
191
|
_this.log(`⥠[FETCH] Intercepting: ${url}`);
|
|
171
192
|
const start = Date.now();
|
|
172
193
|
|
|
173
|
-
//
|
|
194
|
+
// 2. Determine Turn Logic (Your Snippet)
|
|
195
|
+
let activeTurn;
|
|
196
|
+
if (_this.lastTurnConfirmed) {
|
|
197
|
+
activeTurn = _this.turnIndex++;
|
|
198
|
+
_this.lastTurnConfirmed = false;
|
|
199
|
+
} else {
|
|
200
|
+
activeTurn = _this.turnIndex > 0 ? _this.turnIndex - 1 : 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
174
203
|
const { provider, model } = _this.normalize(url, bodyStr);
|
|
204
|
+
const pHash = _this.hash(bodyStr);
|
|
205
|
+
|
|
206
|
+
// 3. Emit Request (User)
|
|
175
207
|
_this.emit({
|
|
176
208
|
session_id: global.__dropout_session_id__,
|
|
177
|
-
turn_index:
|
|
209
|
+
turn_index: activeTurn,
|
|
178
210
|
turn_role: 'user',
|
|
179
211
|
provider,
|
|
180
212
|
model,
|
|
181
213
|
content: _this.privacy === 'full' ? bodyStr : null,
|
|
182
|
-
|
|
214
|
+
content_hash: pHash,
|
|
215
|
+
metadata_flags: {
|
|
216
|
+
retry_like: pHash === _this.lastPromptHash ? 1 : 0,
|
|
217
|
+
method: 'FETCH',
|
|
218
|
+
url: url
|
|
219
|
+
}
|
|
183
220
|
});
|
|
221
|
+
_this.lastPromptHash = pHash;
|
|
184
222
|
|
|
185
|
-
//
|
|
223
|
+
// 4. Actual Network Call
|
|
186
224
|
let response;
|
|
187
225
|
try {
|
|
188
226
|
response = await originalFetch.apply(this, arguments);
|
|
@@ -190,20 +228,32 @@ class Dropout {
|
|
|
190
228
|
|
|
191
229
|
const latency = Date.now() - start;
|
|
192
230
|
|
|
193
|
-
// Emit Response
|
|
231
|
+
// 5. Emit Response (Assistant)
|
|
194
232
|
try {
|
|
195
233
|
const cloned = response.clone();
|
|
196
|
-
|
|
234
|
+
let oText = await cloned.text();
|
|
235
|
+
if (oText && oText.length > _this.maxOutputBytes) {
|
|
236
|
+
oText = oText.slice(0, _this.maxOutputBytes);
|
|
237
|
+
}
|
|
238
|
+
const oHash = _this.hash(oText);
|
|
239
|
+
|
|
197
240
|
_this.emit({
|
|
198
241
|
session_id: global.__dropout_session_id__,
|
|
199
|
-
turn_index:
|
|
242
|
+
turn_index: activeTurn,
|
|
200
243
|
turn_role: 'assistant',
|
|
201
244
|
latency_ms: latency,
|
|
202
245
|
provider,
|
|
203
246
|
model,
|
|
204
247
|
content: _this.privacy === 'full' ? oText : null,
|
|
205
|
-
|
|
248
|
+
content_hash: oHash,
|
|
249
|
+
metadata_flags: {
|
|
250
|
+
non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0,
|
|
251
|
+
turn_boundary_confirmed: 1,
|
|
252
|
+
status: response.status
|
|
253
|
+
}
|
|
206
254
|
});
|
|
255
|
+
_this.lastResponseHash = oHash;
|
|
256
|
+
_this.lastTurnConfirmed = true;
|
|
207
257
|
} catch (e) { _this.log("â ī¸ Failed to read response body"); }
|
|
208
258
|
|
|
209
259
|
return response;
|
|
@@ -216,7 +266,6 @@ class Dropout {
|
|
|
216
266
|
const originalRequest = module.request;
|
|
217
267
|
module.request = function (...args) {
|
|
218
268
|
let url;
|
|
219
|
-
// Argument Resolution (url, options, callback) or (options, callback)
|
|
220
269
|
if (typeof args[0] === 'string') {
|
|
221
270
|
url = args[0];
|
|
222
271
|
} else {
|
|
@@ -235,69 +284,92 @@ class Dropout {
|
|
|
235
284
|
const start = Date.now();
|
|
236
285
|
|
|
237
286
|
const clientRequest = originalRequest.apply(this, args);
|
|
238
|
-
|
|
239
|
-
// Capture Buffers
|
|
240
287
|
const reqChunks = [];
|
|
241
288
|
const originalWrite = clientRequest.write;
|
|
242
289
|
const originalEnd = clientRequest.end;
|
|
243
290
|
|
|
244
|
-
//
|
|
291
|
+
// Buffer Request Data
|
|
245
292
|
clientRequest.write = function (...writeArgs) {
|
|
246
293
|
const chunk = writeArgs[0];
|
|
247
|
-
// Only buffer if it's data (String or Buffer), ignore objects/callbacks
|
|
248
294
|
if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
|
|
249
295
|
reqChunks.push(Buffer.from(chunk));
|
|
250
296
|
}
|
|
251
297
|
return originalWrite.apply(this, writeArgs);
|
|
252
298
|
};
|
|
253
299
|
|
|
254
|
-
// SAFE END PATCH
|
|
255
300
|
clientRequest.end = function (...endArgs) {
|
|
256
301
|
const chunk = endArgs[0];
|
|
257
|
-
// Only buffer if it's data
|
|
258
302
|
if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
|
|
259
303
|
reqChunks.push(Buffer.from(chunk));
|
|
260
304
|
}
|
|
261
305
|
|
|
262
|
-
//
|
|
306
|
+
// --- EMIT REQUEST (On End) ---
|
|
263
307
|
const reqBody = Buffer.concat(reqChunks).toString('utf8');
|
|
264
308
|
const { provider, model } = _this.normalize(url, reqBody);
|
|
309
|
+
const pHash = _this.hash(reqBody);
|
|
310
|
+
|
|
311
|
+
// Turn Logic
|
|
312
|
+
let activeTurn;
|
|
313
|
+
if (_this.lastTurnConfirmed) {
|
|
314
|
+
activeTurn = _this.turnIndex++;
|
|
315
|
+
_this.lastTurnConfirmed = false;
|
|
316
|
+
} else {
|
|
317
|
+
activeTurn = _this.turnIndex > 0 ? _this.turnIndex - 1 : 0;
|
|
318
|
+
}
|
|
265
319
|
|
|
266
|
-
//
|
|
267
|
-
clientRequest.
|
|
320
|
+
// Save state for response
|
|
321
|
+
clientRequest._dropout_turn = activeTurn;
|
|
322
|
+
clientRequest._dropout_provider = provider;
|
|
323
|
+
clientRequest._dropout_model = model;
|
|
268
324
|
|
|
269
325
|
_this.emit({
|
|
270
326
|
session_id: global.__dropout_session_id__,
|
|
271
|
-
turn_index:
|
|
327
|
+
turn_index: activeTurn,
|
|
272
328
|
turn_role: 'user',
|
|
273
329
|
provider,
|
|
274
330
|
model,
|
|
275
331
|
content: _this.privacy === 'full' ? reqBody : null,
|
|
276
|
-
|
|
332
|
+
content_hash: pHash,
|
|
333
|
+
metadata_flags: {
|
|
334
|
+
retry_like: pHash === _this.lastPromptHash ? 1 : 0,
|
|
335
|
+
method: moduleName.toUpperCase(),
|
|
336
|
+
url: url
|
|
337
|
+
}
|
|
277
338
|
});
|
|
339
|
+
_this.lastPromptHash = pHash;
|
|
278
340
|
|
|
279
341
|
return originalEnd.apply(this, endArgs);
|
|
280
342
|
};
|
|
281
343
|
|
|
282
|
-
//
|
|
344
|
+
// --- EMIT RESPONSE ---
|
|
283
345
|
clientRequest.on('response', (res) => {
|
|
284
346
|
const resChunks = [];
|
|
285
347
|
res.on('data', (chunk) => resChunks.push(chunk));
|
|
286
348
|
res.on('end', () => {
|
|
287
|
-
|
|
349
|
+
let resBody = Buffer.concat(resChunks).toString('utf8');
|
|
350
|
+
if (resBody && resBody.length > _this.maxOutputBytes) {
|
|
351
|
+
resBody = resBody.slice(0, _this.maxOutputBytes);
|
|
352
|
+
}
|
|
288
353
|
const latency = Date.now() - start;
|
|
289
|
-
const
|
|
354
|
+
const oHash = _this.hash(resBody);
|
|
290
355
|
|
|
291
356
|
_this.emit({
|
|
292
357
|
session_id: global.__dropout_session_id__,
|
|
293
|
-
turn_index: 0,
|
|
358
|
+
turn_index: clientRequest._dropout_turn || 0,
|
|
294
359
|
turn_role: 'assistant',
|
|
295
360
|
latency_ms: latency,
|
|
296
|
-
provider:
|
|
297
|
-
model:
|
|
361
|
+
provider: clientRequest._dropout_provider,
|
|
362
|
+
model: clientRequest._dropout_model,
|
|
298
363
|
content: _this.privacy === 'full' ? resBody : null,
|
|
299
|
-
|
|
364
|
+
content_hash: oHash,
|
|
365
|
+
metadata_flags: {
|
|
366
|
+
status: res.statusCode,
|
|
367
|
+
non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0,
|
|
368
|
+
}
|
|
300
369
|
});
|
|
370
|
+
|
|
371
|
+
_this.lastResponseHash = oHash;
|
|
372
|
+
_this.lastTurnConfirmed = true;
|
|
301
373
|
});
|
|
302
374
|
});
|
|
303
375
|
|