@dropout-ai/runtime 0.3.6 → 0.3.7

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 +188 -176
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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,13 +1,11 @@
1
1
  /**
2
2
  * @dropout-ai/runtime
3
3
  * Universal AI Interaction Capture for Node.js
4
- * Patches: fetch, http.request, https.request
5
- * Behavior: Splits interactions into separate User/Assistant rows for full analytics.
4
+ * Stability: High (Browser-Safe, Silent Failures)
6
5
  */
7
6
 
8
- const https = require('https');
9
- const http = require('http');
10
- const crypto = require('crypto');
7
+ // We define these at the top scope but require them lazily or safely
8
+ let https, http, crypto;
11
9
 
12
10
  // --- DEFAULT CONFIGURATION ---
13
11
  const SUPABASE_FUNCTION_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
@@ -20,52 +18,73 @@ const KNOWN_AI_DOMAINS = [
20
18
 
21
19
  class Dropout {
22
20
  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.");
21
+ // đŸ›Ąī¸ 1. SAFETY FUSE: BROWSER CHECK
22
+ // If this runs in a browser (Client Component), ABORT IMMEDIATELY.
23
+ // This prevents the "listener" error and protects your app from crashing.
24
+ if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
25
+ if (config.debug) console.warn("[Dropout] âš ī¸ Skipped: SDK detected browser environment. Running in Server-Mode only.");
26
26
  return;
27
27
  }
28
28
 
29
- // 1. Validation
29
+ // đŸ›Ąī¸ 2. SAFETY FUSE: MISSING CREDENTIALS
30
30
  if (!config.apiKey || !config.projectId) {
31
- console.warn("[Dropout] âš ī¸ Initialization Skipped: Missing apiKey or projectId.");
31
+ if (config.debug) console.warn("[Dropout] âš ī¸ Skipped: Missing API Key or Project ID.");
32
32
  return;
33
33
  }
34
34
 
35
- // 2. Config & State Setup
36
- this.config = config;
37
- this.projectId = config.projectId;
38
- this.apiKey = config.apiKey;
39
- this.debug = config.debug || false;
40
- this.privacy = config.privacy || 'full';
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;
49
-
50
- // 4. Singleton Guard
51
- if (global.__dropout_initialized__) {
52
- if (this.debug) console.log("[Dropout] â„šī¸ Already initialized. Skipping patch.");
35
+ // đŸ›Ąī¸ 3. SAFETY FUSE: DEPENDENCY CHECK
36
+ // We try to load Node modules. If this fails (rare runtime edge case), we abort.
37
+ try {
38
+ https = require('https');
39
+ http = require('http');
40
+ crypto = require('crypto');
41
+ } catch (e) {
42
+ console.warn("[Dropout] âš ī¸ Skipped: Node.js core modules missing.");
53
43
  return;
54
44
  }
55
- global.__dropout_initialized__ = true;
56
45
 
57
- // 5. Initialize Identity
58
- if (!global.__dropout_session_id__) {
59
- global.__dropout_session_id__ = this.generateSessionId();
60
- }
46
+ // --- INITIALIZATION ---
47
+ try {
48
+ this.config = config;
49
+ this.projectId = config.projectId;
50
+ this.apiKey = config.apiKey;
51
+ this.debug = config.debug || false;
52
+ this.privacy = config.privacy || 'full';
53
+ this.captureEndpoint = SUPABASE_FUNCTION_URL;
54
+ this.maxOutputBytes = 32768;
55
+
56
+ // Conversation State
57
+ this.turnIndex = 0;
58
+ this.lastTurnConfirmed = true;
59
+ this.lastPromptHash = null;
60
+ this.lastResponseHash = null;
61
+
62
+ // Singleton Guard
63
+ if (global.__dropout_initialized__) {
64
+ if (this.debug) console.log("[Dropout] â„šī¸ Already initialized.");
65
+ return;
66
+ }
67
+ global.__dropout_initialized__ = true;
68
+
69
+ // Identity
70
+ if (!global.__dropout_session_id__) {
71
+ global.__dropout_session_id__ = this.generateSessionId();
72
+ }
61
73
 
62
- // 6. Start
63
- if (this.debug) console.log(`[Dropout] đŸŸĸ Initialized for Project: ${this.projectId}`);
74
+ if (this.debug) console.log(`[Dropout] đŸŸĸ Online: ${this.projectId}`);
64
75
 
65
- // Bind methods
66
- this.log = this.log.bind(this);
67
- this.emit = this.emit.bind(this);
68
- this.patchNetwork();
76
+ // Bindings
77
+ this.log = this.log.bind(this);
78
+ this.emit = this.emit.bind(this);
79
+
80
+ // đŸ›Ąī¸ 4. SAFETY FUSE: NETWORK PATCHING
81
+ // If patching fails, we log it and continue. We DO NOT crash the app.
82
+ this.patchNetwork();
83
+
84
+ } catch (err) {
85
+ // THE ULTIMATE CATCH-ALL
86
+ console.error("[Dropout] ❌ Critical Initialization Error (App will continue):", err);
87
+ }
69
88
  }
70
89
 
71
90
  // --- UTILS ---
@@ -91,26 +110,24 @@ class Dropout {
91
110
  normalize(url, body) {
92
111
  let provider = 'custom';
93
112
  let model = 'unknown';
113
+ try {
114
+ if (url) {
115
+ const u = url.toLowerCase();
116
+ if (u.includes('openai')) provider = 'openai';
117
+ else if (u.includes('anthropic')) provider = 'anthropic';
118
+ else if (u.includes('google') || u.includes('gemini') || u.includes('aiplatform')) provider = 'google';
119
+ else if (u.includes('groq')) provider = 'groq';
120
+ else if (u.includes('mistral')) provider = 'mistral';
121
+ else if (u.includes('cohere')) provider = 'cohere';
122
+ else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
123
+ }
94
124
 
95
- if (url) {
96
- const u = url.toLowerCase();
97
- if (u.includes('openai')) provider = 'openai';
98
- else if (u.includes('anthropic')) provider = 'anthropic';
99
- else if (u.includes('google') || u.includes('gemini') || u.includes('aiplatform')) provider = 'google';
100
- else if (u.includes('groq')) provider = 'groq';
101
- else if (u.includes('mistral')) provider = 'mistral';
102
- else if (u.includes('cohere')) provider = 'cohere';
103
- else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
104
- }
105
-
106
- if (body) {
107
- try {
125
+ if (body) {
108
126
  const parsed = typeof body === 'string' ? JSON.parse(body) : body;
109
127
  if (parsed.model) model = parsed.model;
110
- // Simple heuristic fallback if model is missing but structure looks AI-ish
111
128
  if (provider === 'custom' && (parsed.messages || parsed.prompt)) provider = 'heuristic';
112
- } catch (e) { }
113
- }
129
+ }
130
+ } catch (e) { /* Ignore parsing errors safely */ }
114
131
  return { provider, model };
115
132
  }
116
133
 
@@ -129,43 +146,41 @@ class Dropout {
129
146
 
130
147
  // --- EMITTER ---
131
148
  emit(payload) {
132
- if (!this.apiKey || !this.projectId) return;
133
-
134
- // Construct Final Payload matching your Inbox Table
135
- const finalPayload = {
136
- project_id: this.projectId,
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,
153
- received_at: new Date().toISOString()
154
- };
149
+ try {
150
+ if (!this.apiKey || !this.projectId) return;
151
+
152
+ const finalPayload = {
153
+ project_id: this.projectId,
154
+ session_id: payload.session_id,
155
+ turn_index: payload.turn_index,
156
+ turn_role: payload.turn_role,
157
+ provider: payload.provider,
158
+ model: payload.model,
159
+ latency_ms: payload.latency_ms || null,
160
+ content: payload.content,
161
+ //content_blob: payload.content,
162
+ content_hash: payload.content_hash,
163
+ metadata_flags: payload.metadata_flags,
164
+ received_at: new Date().toISOString()
165
+ };
155
166
 
156
- this.log(`🚀 Sending Capture [${payload.turn_role}]`);
167
+ this.log(`🚀 Sending Capture [${payload.turn_role}]`);
157
168
 
158
- const req = https.request(this.captureEndpoint, {
159
- method: 'POST',
160
- headers: {
161
- 'Content-Type': 'application/json',
162
- 'x-dropout-key': this.apiKey
163
- }
164
- });
169
+ // Use pure Node request to bypass patches
170
+ const req = https.request(this.captureEndpoint, {
171
+ method: 'POST',
172
+ headers: {
173
+ 'Content-Type': 'application/json',
174
+ 'x-dropout-key': this.apiKey
175
+ }
176
+ });
165
177
 
166
- req.on('error', (e) => this.log("❌ Upload Failed", e.message));
167
- req.write(JSON.stringify(finalPayload));
168
- req.end();
178
+ req.on('error', (e) => this.log("❌ Upload Failed (Silent)", e.message));
179
+ req.write(JSON.stringify(finalPayload));
180
+ req.end();
181
+ } catch (e) {
182
+ this.log("âš ī¸ Emit Error (Silent)", e);
183
+ }
169
184
  }
170
185
 
171
186
  // --- CORE PATCHING ---
@@ -176,14 +191,17 @@ class Dropout {
176
191
  if (global.fetch) {
177
192
  const originalFetch = global.fetch;
178
193
  global.fetch = async function (input, init) {
179
- const url = typeof input === 'string' ? input : input?.url;
194
+ // 1. Safe URL Extraction
195
+ let url;
196
+ try { url = typeof input === 'string' ? input : input?.url; } catch (e) { return originalFetch.apply(this, arguments); }
180
197
 
198
+ // 2. Safe Body Extraction
181
199
  let bodyStr = "";
182
200
  if (init && init.body) {
183
201
  try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
184
202
  }
185
203
 
186
- // 1. Guard
204
+ // 3. Guard
187
205
  if (!_this.isAiRequest(url, bodyStr)) {
188
206
  return originalFetch.apply(this, arguments);
189
207
  }
@@ -191,7 +209,7 @@ class Dropout {
191
209
  _this.log(`⚡ [FETCH] Intercepting: ${url}`);
192
210
  const start = Date.now();
193
211
 
194
- // 2. Determine Turn Logic (Your Snippet)
212
+ // 4. Determine Turn
195
213
  let activeTurn;
196
214
  if (_this.lastTurnConfirmed) {
197
215
  activeTurn = _this.turnIndex++;
@@ -203,7 +221,7 @@ class Dropout {
203
221
  const { provider, model } = _this.normalize(url, bodyStr);
204
222
  const pHash = _this.hash(bodyStr);
205
223
 
206
- // 3. Emit Request (User)
224
+ // 5. Emit User Request
207
225
  _this.emit({
208
226
  session_id: global.__dropout_session_id__,
209
227
  turn_index: activeTurn,
@@ -212,15 +230,11 @@ class Dropout {
212
230
  model,
213
231
  content: _this.privacy === 'full' ? bodyStr : null,
214
232
  content_hash: pHash,
215
- metadata_flags: {
216
- retry_like: pHash === _this.lastPromptHash ? 1 : 0,
217
- method: 'FETCH',
218
- url: url
219
- }
233
+ metadata_flags: { retry_like: pHash === _this.lastPromptHash ? 1 : 0, method: 'FETCH', url: url }
220
234
  });
221
235
  _this.lastPromptHash = pHash;
222
236
 
223
- // 4. Actual Network Call
237
+ // 6. Execute Original (Wrapped)
224
238
  let response;
225
239
  try {
226
240
  response = await originalFetch.apply(this, arguments);
@@ -228,7 +242,7 @@ class Dropout {
228
242
 
229
243
  const latency = Date.now() - start;
230
244
 
231
- // 5. Emit Response (Assistant)
245
+ // 7. Emit AI Response (Non-Blocking)
232
246
  try {
233
247
  const cloned = response.clone();
234
248
  let oText = await cloned.text();
@@ -266,15 +280,17 @@ class Dropout {
266
280
  const originalRequest = module.request;
267
281
  module.request = function (...args) {
268
282
  let url;
269
- if (typeof args[0] === 'string') {
270
- url = args[0];
271
- } else {
272
- const opts = args[0] || {};
273
- const protocol = opts.protocol || (moduleName === 'https' ? 'https:' : 'http:');
274
- const host = opts.hostname || opts.host || 'localhost';
275
- const path = opts.path || '/';
276
- url = `${protocol}//${host}${path}`;
277
- }
283
+ try {
284
+ if (typeof args[0] === 'string') {
285
+ url = args[0];
286
+ } else {
287
+ const opts = args[0] || {};
288
+ const protocol = opts.protocol || (moduleName === 'https' ? 'https:' : 'http:');
289
+ const host = opts.hostname || opts.host || 'localhost';
290
+ const path = opts.path || '/';
291
+ url = `${protocol}//${host}${path}`;
292
+ }
293
+ } catch (e) { return originalRequest.apply(this, args); }
278
294
 
279
295
  if (!_this.isAiRequest(url, null)) {
280
296
  return originalRequest.apply(this, args);
@@ -288,88 +304,84 @@ class Dropout {
288
304
  const originalWrite = clientRequest.write;
289
305
  const originalEnd = clientRequest.end;
290
306
 
291
- // Buffer Request Data
307
+ // SAFE WRITE PATCH
292
308
  clientRequest.write = function (...writeArgs) {
293
- const chunk = writeArgs[0];
294
- if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
295
- reqChunks.push(Buffer.from(chunk));
296
- }
309
+ try {
310
+ const chunk = writeArgs[0];
311
+ if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
312
+ reqChunks.push(Buffer.from(chunk));
313
+ }
314
+ } catch (e) { }
297
315
  return originalWrite.apply(this, writeArgs);
298
316
  };
299
317
 
318
+ // SAFE END PATCH
300
319
  clientRequest.end = function (...endArgs) {
301
- const chunk = endArgs[0];
302
- if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
303
- reqChunks.push(Buffer.from(chunk));
304
- }
320
+ try {
321
+ const chunk = endArgs[0];
322
+ if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
323
+ reqChunks.push(Buffer.from(chunk));
324
+ }
305
325
 
306
- // --- EMIT REQUEST (On End) ---
307
- const reqBody = Buffer.concat(reqChunks).toString('utf8');
308
- const { provider, model } = _this.normalize(url, reqBody);
309
- const pHash = _this.hash(reqBody);
326
+ const reqBody = Buffer.concat(reqChunks).toString('utf8');
327
+ const { provider, model } = _this.normalize(url, reqBody);
328
+ const pHash = _this.hash(reqBody);
310
329
 
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
- }
330
+ let activeTurn;
331
+ if (_this.lastTurnConfirmed) {
332
+ activeTurn = _this.turnIndex++;
333
+ _this.lastTurnConfirmed = false;
334
+ } else {
335
+ activeTurn = _this.turnIndex > 0 ? _this.turnIndex - 1 : 0;
336
+ }
319
337
 
320
- // Save state for response
321
- clientRequest._dropout_turn = activeTurn;
322
- clientRequest._dropout_provider = provider;
323
- clientRequest._dropout_model = model;
338
+ clientRequest._dropout_turn = activeTurn;
339
+ clientRequest._dropout_provider = provider;
340
+ clientRequest._dropout_model = model;
324
341
 
325
- _this.emit({
326
- session_id: global.__dropout_session_id__,
327
- turn_index: activeTurn,
328
- turn_role: 'user',
329
- provider,
330
- model,
331
- content: _this.privacy === 'full' ? reqBody : null,
332
- content_hash: pHash,
333
- metadata_flags: {
334
- retry_like: pHash === _this.lastPromptHash ? 1 : 0,
335
- method: moduleName.toUpperCase(),
336
- url: url
337
- }
338
- });
339
- _this.lastPromptHash = pHash;
342
+ _this.emit({
343
+ session_id: global.__dropout_session_id__,
344
+ turn_index: activeTurn,
345
+ turn_role: 'user',
346
+ provider,
347
+ model,
348
+ content: _this.privacy === 'full' ? reqBody : null,
349
+ content_hash: pHash,
350
+ metadata_flags: { retry_like: pHash === _this.lastPromptHash ? 1 : 0, method: moduleName.toUpperCase(), url: url }
351
+ });
352
+ _this.lastPromptHash = pHash;
353
+ } catch (e) { _this.log("âš ī¸ Request Capture Failed"); }
340
354
 
341
355
  return originalEnd.apply(this, endArgs);
342
356
  };
343
357
 
344
- // --- EMIT RESPONSE ---
358
+ // SAFE RESPONSE LISTENER
345
359
  clientRequest.on('response', (res) => {
346
360
  const resChunks = [];
347
361
  res.on('data', (chunk) => resChunks.push(chunk));
348
362
  res.on('end', () => {
349
- let resBody = Buffer.concat(resChunks).toString('utf8');
350
- if (resBody && resBody.length > _this.maxOutputBytes) {
351
- resBody = resBody.slice(0, _this.maxOutputBytes);
352
- }
353
- const latency = Date.now() - start;
354
- const oHash = _this.hash(resBody);
355
-
356
- _this.emit({
357
- session_id: global.__dropout_session_id__,
358
- turn_index: clientRequest._dropout_turn || 0,
359
- turn_role: 'assistant',
360
- latency_ms: latency,
361
- provider: clientRequest._dropout_provider,
362
- model: clientRequest._dropout_model,
363
- content: _this.privacy === 'full' ? resBody : null,
364
- content_hash: oHash,
365
- metadata_flags: {
366
- status: res.statusCode,
367
- non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0,
368
- }
369
- });
370
-
371
- _this.lastResponseHash = oHash;
372
- _this.lastTurnConfirmed = true;
363
+ try {
364
+ let resBody = Buffer.concat(resChunks).toString('utf8');
365
+ if (resBody && resBody.length > _this.maxOutputBytes) resBody = resBody.slice(0, _this.maxOutputBytes);
366
+
367
+ const latency = Date.now() - start;
368
+ const oHash = _this.hash(resBody);
369
+
370
+ _this.emit({
371
+ session_id: global.__dropout_session_id__,
372
+ turn_index: clientRequest._dropout_turn || 0,
373
+ turn_role: 'assistant',
374
+ latency_ms: latency,
375
+ provider: clientRequest._dropout_provider,
376
+ model: clientRequest._dropout_model,
377
+ content: _this.privacy === 'full' ? resBody : null,
378
+ content_hash: oHash,
379
+ metadata_flags: { status: res.statusCode, non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0 }
380
+ });
381
+
382
+ _this.lastResponseHash = oHash;
383
+ _this.lastTurnConfirmed = true;
384
+ } catch (e) { _this.log("âš ī¸ Response Capture Failed"); }
373
385
  });
374
386
  });
375
387
 
@@ -383,8 +395,8 @@ class Dropout {
383
395
  }
384
396
  }
385
397
 
386
- // Auto-Start (Node Preload)
387
- if (process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
398
+ // Auto-Start (Server-Side Only)
399
+ if (typeof process !== 'undefined' && process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
388
400
  new Dropout({
389
401
  projectId: process.env.DROPOUT_PROJECT_ID,
390
402
  apiKey: process.env.DROPOUT_API_KEY,