@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +122 -50
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Invisible Node.js runtime for capturing AI interactions.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @dropout-ai/runtime
3
- * Universal AI Interaction Capture for Node.js & Next.js
3
+ * Universal AI Interaction Capture for Node.js
4
4
  * Patches: fetch, http.request, https.request
5
- * Capability: Captures Genkit, OpenAI, LangChain, Axios, and standard fetch.
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 for Auto-Detection
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
- // 3. Singleton Guard
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
- // 4. Initialize Identity
57
+ // 5. Initialize Identity
45
58
  if (!global.__dropout_session_id__) {
46
59
  global.__dropout_session_id__ = this.generateSessionId();
47
60
  }
48
61
 
49
- // 5. Start the Wiretap
62
+ // 6. Start
50
63
  if (this.debug) console.log(`[Dropout] đŸŸĸ Initialized for Project: ${this.projectId}`);
51
64
 
52
- // Bind methods to 'this' to avoid scope loss
65
+ // Bind methods
53
66
  this.log = this.log.bind(this);
54
- this.isAiRequest = this.isAiRequest.bind(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
- // 2. Construct Payload
134
+ // Construct Final Payload matching your Inbox Table
124
135
  const finalPayload = {
125
- ...payload,
126
136
  project_id: this.projectId,
127
- content_blob: payload.content, // Map for new schema
128
- content: payload.content, // Map for old schema (fallback)
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 (${payload.turn_role})`);
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; // Capture instance
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
- // Emit Request
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: 0,
209
+ turn_index: activeTurn,
178
210
  turn_role: 'user',
179
211
  provider,
180
212
  model,
181
213
  content: _this.privacy === 'full' ? bodyStr : null,
182
- metadata_flags: { method: 'FETCH', url: url }
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
- // Execute
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
- const oText = await cloned.text();
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: 0,
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
- metadata_flags: { status: response.status }
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
- // SAFE WRITE PATCH
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
- // Emit Request
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
- // Attach state to request for response handler
267
- clientRequest._dropout_meta = { provider, model };
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: 0,
327
+ turn_index: activeTurn,
272
328
  turn_role: 'user',
273
329
  provider,
274
330
  model,
275
331
  content: _this.privacy === 'full' ? reqBody : null,
276
- metadata_flags: { method: moduleName.toUpperCase(), url: url }
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
- // Capture Response
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
- const resBody = Buffer.concat(resChunks).toString('utf8');
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 meta = clientRequest._dropout_meta || {};
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: meta.provider,
297
- model: meta.model,
361
+ provider: clientRequest._dropout_provider,
362
+ model: clientRequest._dropout_model,
298
363
  content: _this.privacy === 'full' ? resBody : null,
299
- metadata_flags: { status: res.statusCode }
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