@dropout-ai/runtime 0.3.5 → 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 +114 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.3.5",
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'
@@ -21,8 +21,6 @@ const KNOWN_AI_DOMAINS = [
21
21
  class Dropout {
22
22
  constructor(config = {}) {
23
23
  // 🚨 BROWSER GUARD 🚨
24
- // If this runs in the browser (Client Component), do nothing.
25
- // This prevents the "listener" error and protects your API keys.
26
24
  if (typeof window !== 'undefined') {
27
25
  console.warn("[Dropout] âš ī¸ Initialization Skipped: This SDK is for Node.js/Server environments only.");
28
26
  return;
@@ -34,32 +32,39 @@ class Dropout {
34
32
  return;
35
33
  }
36
34
 
37
- // 2. Config Setup
35
+ // 2. Config & State Setup
38
36
  this.config = config;
39
37
  this.projectId = config.projectId;
40
38
  this.apiKey = config.apiKey;
41
39
  this.debug = config.debug || false;
42
40
  this.privacy = config.privacy || 'full';
43
41
  this.captureEndpoint = SUPABASE_FUNCTION_URL;
42
+ this.maxOutputBytes = 32768;
44
43
 
45
- // 3. Singleton Guard
44
+ // 3. Conversation State (Matches your snippet)
45
+ this.turnIndex = 0;
46
+ this.lastTurnConfirmed = true;
47
+ this.lastPromptHash = null;
48
+ this.lastResponseHash = null;
49
+
50
+ // 4. Singleton Guard
46
51
  if (global.__dropout_initialized__) {
47
52
  if (this.debug) console.log("[Dropout] â„šī¸ Already initialized. Skipping patch.");
48
53
  return;
49
54
  }
50
55
  global.__dropout_initialized__ = true;
51
56
 
52
- // 4. Initialize Identity
57
+ // 5. Initialize Identity
53
58
  if (!global.__dropout_session_id__) {
54
59
  global.__dropout_session_id__ = this.generateSessionId();
55
60
  }
56
61
 
57
- // 5. Start the Wiretap
62
+ // 6. Start
58
63
  if (this.debug) console.log(`[Dropout] đŸŸĸ Initialized for Project: ${this.projectId}`);
59
64
 
60
65
  // Bind methods
61
66
  this.log = this.log.bind(this);
62
- this.isAiRequest = this.isAiRequest.bind(this);
67
+ this.emit = this.emit.bind(this);
63
68
  this.patchNetwork();
64
69
  }
65
70
 
@@ -102,6 +107,8 @@ class Dropout {
102
107
  try {
103
108
  const parsed = typeof body === 'string' ? JSON.parse(body) : body;
104
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';
105
112
  } catch (e) { }
106
113
  }
107
114
  return { provider, model };
@@ -124,15 +131,29 @@ class Dropout {
124
131
  emit(payload) {
125
132
  if (!this.apiKey || !this.projectId) return;
126
133
 
134
+ // Construct Final Payload matching your Inbox Table
127
135
  const finalPayload = {
128
- ...payload,
129
136
  project_id: this.projectId,
130
- //content_blob: payload.content,
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)
131
147
  content: payload.content,
148
+ //content_blob: payload.content,
149
+ content_hash: payload.content_hash,
150
+
151
+ // Metadata
152
+ metadata_flags: payload.metadata_flags,
132
153
  received_at: new Date().toISOString()
133
154
  };
134
155
 
135
- this.log(`🚀 Sending Capture (${payload.turn_role})`);
156
+ this.log(`🚀 Sending Capture [${payload.turn_role}]`);
136
157
 
137
158
  const req = https.request(this.captureEndpoint, {
138
159
  method: 'POST',
@@ -162,6 +183,7 @@ class Dropout {
162
183
  try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
163
184
  }
164
185
 
186
+ // 1. Guard
165
187
  if (!_this.isAiRequest(url, bodyStr)) {
166
188
  return originalFetch.apply(this, arguments);
167
189
  }
@@ -169,17 +191,36 @@ class Dropout {
169
191
  _this.log(`⚡ [FETCH] Intercepting: ${url}`);
170
192
  const start = Date.now();
171
193
 
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
+
172
203
  const { provider, model } = _this.normalize(url, bodyStr);
204
+ const pHash = _this.hash(bodyStr);
205
+
206
+ // 3. Emit Request (User)
173
207
  _this.emit({
174
208
  session_id: global.__dropout_session_id__,
175
- turn_index: 0,
209
+ turn_index: activeTurn,
176
210
  turn_role: 'user',
177
211
  provider,
178
212
  model,
179
213
  content: _this.privacy === 'full' ? bodyStr : null,
180
- 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
+ }
181
220
  });
221
+ _this.lastPromptHash = pHash;
182
222
 
223
+ // 4. Actual Network Call
183
224
  let response;
184
225
  try {
185
226
  response = await originalFetch.apply(this, arguments);
@@ -187,19 +228,32 @@ class Dropout {
187
228
 
188
229
  const latency = Date.now() - start;
189
230
 
231
+ // 5. Emit Response (Assistant)
190
232
  try {
191
233
  const cloned = response.clone();
192
- 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
+
193
240
  _this.emit({
194
241
  session_id: global.__dropout_session_id__,
195
- turn_index: 0,
242
+ turn_index: activeTurn,
196
243
  turn_role: 'assistant',
197
244
  latency_ms: latency,
198
245
  provider,
199
246
  model,
200
247
  content: _this.privacy === 'full' ? oText : null,
201
- 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
+ }
202
254
  });
255
+ _this.lastResponseHash = oHash;
256
+ _this.lastTurnConfirmed = true;
203
257
  } catch (e) { _this.log("âš ī¸ Failed to read response body"); }
204
258
 
205
259
  return response;
@@ -234,6 +288,7 @@ class Dropout {
234
288
  const originalWrite = clientRequest.write;
235
289
  const originalEnd = clientRequest.end;
236
290
 
291
+ // Buffer Request Data
237
292
  clientRequest.write = function (...writeArgs) {
238
293
  const chunk = writeArgs[0];
239
294
  if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
@@ -248,41 +303,73 @@ class Dropout {
248
303
  reqChunks.push(Buffer.from(chunk));
249
304
  }
250
305
 
306
+ // --- EMIT REQUEST (On End) ---
251
307
  const reqBody = Buffer.concat(reqChunks).toString('utf8');
252
308
  const { provider, model } = _this.normalize(url, reqBody);
253
- clientRequest._dropout_meta = { provider, model };
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
+ }
319
+
320
+ // Save state for response
321
+ clientRequest._dropout_turn = activeTurn;
322
+ clientRequest._dropout_provider = provider;
323
+ clientRequest._dropout_model = model;
254
324
 
255
325
  _this.emit({
256
326
  session_id: global.__dropout_session_id__,
257
- turn_index: 0,
327
+ turn_index: activeTurn,
258
328
  turn_role: 'user',
259
329
  provider,
260
330
  model,
261
331
  content: _this.privacy === 'full' ? reqBody : null,
262
- 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
+ }
263
338
  });
339
+ _this.lastPromptHash = pHash;
264
340
 
265
341
  return originalEnd.apply(this, endArgs);
266
342
  };
267
343
 
344
+ // --- EMIT RESPONSE ---
268
345
  clientRequest.on('response', (res) => {
269
346
  const resChunks = [];
270
347
  res.on('data', (chunk) => resChunks.push(chunk));
271
348
  res.on('end', () => {
272
- 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
+ }
273
353
  const latency = Date.now() - start;
274
- const meta = clientRequest._dropout_meta || {};
354
+ const oHash = _this.hash(resBody);
275
355
 
276
356
  _this.emit({
277
357
  session_id: global.__dropout_session_id__,
278
- turn_index: 0,
358
+ turn_index: clientRequest._dropout_turn || 0,
279
359
  turn_role: 'assistant',
280
360
  latency_ms: latency,
281
- provider: meta.provider,
282
- model: meta.model,
361
+ provider: clientRequest._dropout_provider,
362
+ model: clientRequest._dropout_model,
283
363
  content: _this.privacy === 'full' ? resBody : null,
284
- 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
+ }
285
369
  });
370
+
371
+ _this.lastResponseHash = oHash;
372
+ _this.lastTurnConfirmed = true;
286
373
  });
287
374
  });
288
375