@dropout-ai/runtime 0.3.6 → 0.3.8

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 +212 -182
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.8",
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)
5
+ * Features: Auto-Discovery, Connectivity Check, Dual-Schema Support
6
6
  */
7
7
 
8
- const https = require('https');
9
- const http = require('http');
10
- const crypto = require('crypto');
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,74 @@ 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. BROWSER GUARD (Client-Side Protection)
22
+ // If this accidentally runs in a browser/Client Component, we abort silently.
23
+ if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
24
+ if (config.debug) console.warn("[Dropout] âš ī¸ Skipped: Browser detected. SDK is Server-Only.");
26
25
  return;
27
26
  }
28
27
 
29
- // 1. Validation
28
+ // đŸ›Ąī¸ 2. CREDENTIAL CHECK
30
29
  if (!config.apiKey || !config.projectId) {
31
- console.warn("[Dropout] âš ī¸ Initialization Skipped: Missing apiKey or projectId.");
30
+ if (config.debug) console.warn("[Dropout] âš ī¸ Skipped: Missing API Key or Project ID.");
32
31
  return;
33
32
  }
34
33
 
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.");
34
+ // đŸ›Ąī¸ 3. DEPENDENCY CHECK
35
+ try {
36
+ https = require('https');
37
+ http = require('http');
38
+ crypto = require('crypto');
39
+ } catch (e) {
40
+ if (config.debug) console.warn("[Dropout] âš ī¸ Skipped: Node core modules missing.");
53
41
  return;
54
42
  }
55
- global.__dropout_initialized__ = true;
56
43
 
57
- // 5. Initialize Identity
58
- if (!global.__dropout_session_id__) {
59
- global.__dropout_session_id__ = this.generateSessionId();
60
- }
44
+ // --- INITIALIZATION ---
45
+ try {
46
+ this.config = config;
47
+ this.projectId = config.projectId;
48
+ this.apiKey = config.apiKey;
49
+ this.debug = config.debug || false;
50
+ this.privacy = config.privacy || 'full';
51
+ this.captureEndpoint = SUPABASE_FUNCTION_URL;
52
+ this.maxOutputBytes = 32768;
53
+
54
+ // State
55
+ this.turnIndex = 0;
56
+ this.lastTurnConfirmed = true;
57
+ this.lastPromptHash = null;
58
+ this.lastResponseHash = null;
59
+
60
+ // Singleton Guard
61
+ if (global.__dropout_initialized__) {
62
+ if (this.debug) console.log("[Dropout] â„šī¸ Already initialized.");
63
+ return;
64
+ }
65
+ global.__dropout_initialized__ = true;
61
66
 
62
- // 6. Start
63
- if (this.debug) console.log(`[Dropout] đŸŸĸ Initialized for Project: ${this.projectId}`);
67
+ // Identity
68
+ if (!global.__dropout_session_id__) {
69
+ global.__dropout_session_id__ = this.generateSessionId();
70
+ }
64
71
 
65
- // Bind methods
66
- this.log = this.log.bind(this);
67
- this.emit = this.emit.bind(this);
68
- this.patchNetwork();
72
+ // Bindings
73
+ this.log = this.log.bind(this);
74
+ this.emit = this.emit.bind(this);
75
+
76
+ // đŸ›Ąī¸ 4. SAFE STARTUP
77
+ this.patchNetwork();
78
+
79
+ // 🔍 5. CONNECTION VERIFICATION (Async Ping)
80
+ // Only runs if debug is TRUE to confirm setup
81
+ if (this.debug) {
82
+ this.validateConnection();
83
+ }
84
+
85
+ } catch (err) {
86
+ // THE ULTIMATE CATCH-ALL: Ensure app NEVER crashes
87
+ if (config.debug) console.error("[Dropout] ❌ Initialization Error:", err);
88
+ }
69
89
  }
70
90
 
71
91
  // --- UTILS ---
@@ -81,6 +101,39 @@ class Dropout {
81
101
  }
82
102
  }
83
103
 
104
+ // --- HEALTH CHECK ---
105
+ validateConnection() {
106
+ this.log("🔄 Verifying connection to Dropout Cloud...");
107
+
108
+ const req = https.request(this.captureEndpoint, {
109
+ method: 'POST',
110
+ headers: {
111
+ 'Content-Type': 'application/json',
112
+ 'x-dropout-key': this.apiKey
113
+ }
114
+ }, (res) => {
115
+ if (res.statusCode >= 200 && res.statusCode < 300) {
116
+ console.log(`[Dropout] ✅ Connected to Project: ${this.projectId}`);
117
+ } else {
118
+ console.warn(`[Dropout] âš ī¸ Connection Failed (Status: ${res.statusCode}). Check your API Key.`);
119
+ }
120
+ });
121
+
122
+ req.on('error', (e) => console.warn(`[Dropout] ❌ Connection Error: ${e.message}`));
123
+
124
+ // Send lightweight Ping payload
125
+ req.write(JSON.stringify({
126
+ project_id: this.projectId,
127
+ session_id: 'system_ping',
128
+ turn_role: 'system',
129
+ turn_index: 0,
130
+ content: 'ping',
131
+ //content_blob: 'ping',
132
+ metadata_flags: { type: 'connectivity_check' }
133
+ }));
134
+ req.end();
135
+ }
136
+
84
137
  hash(text) {
85
138
  if (!text) return null;
86
139
  try {
@@ -91,35 +144,30 @@ class Dropout {
91
144
  normalize(url, body) {
92
145
  let provider = 'custom';
93
146
  let model = 'unknown';
94
-
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 {
147
+ try {
148
+ if (url) {
149
+ const u = url.toLowerCase();
150
+ if (u.includes('openai')) provider = 'openai';
151
+ else if (u.includes('anthropic')) provider = 'anthropic';
152
+ else if (u.includes('google') || u.includes('gemini') || u.includes('aiplatform')) provider = 'google';
153
+ else if (u.includes('groq')) provider = 'groq';
154
+ else if (u.includes('mistral')) provider = 'mistral';
155
+ else if (u.includes('cohere')) provider = 'cohere';
156
+ else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
157
+ }
158
+ if (body) {
108
159
  const parsed = typeof body === 'string' ? JSON.parse(body) : body;
109
160
  if (parsed.model) model = parsed.model;
110
- // Simple heuristic fallback if model is missing but structure looks AI-ish
111
161
  if (provider === 'custom' && (parsed.messages || parsed.prompt)) provider = 'heuristic';
112
- } catch (e) { }
113
- }
162
+ }
163
+ } catch (e) { /* Ignore parsing errors */ }
114
164
  return { provider, model };
115
165
  }
116
166
 
117
167
  isAiRequest(url, bodyString) {
118
168
  if (!url) return false;
119
169
  if (url.includes(this.captureEndpoint)) return false;
120
-
121
170
  if (KNOWN_AI_DOMAINS.some(d => url.includes(d))) return true;
122
-
123
171
  if (bodyString) {
124
172
  return (bodyString.includes('"model"') || bodyString.includes('"messages"')) &&
125
173
  (bodyString.includes('"user"') || bodyString.includes('"prompt"'));
@@ -129,43 +177,40 @@ class Dropout {
129
177
 
130
178
  // --- EMITTER ---
131
179
  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
- };
180
+ try {
181
+ if (!this.apiKey || !this.projectId) return;
182
+
183
+ const finalPayload = {
184
+ project_id: this.projectId,
185
+ session_id: payload.session_id,
186
+ turn_index: payload.turn_index,
187
+ turn_role: payload.turn_role,
188
+ provider: payload.provider,
189
+ model: payload.model,
190
+ latency_ms: payload.latency_ms || null,
191
+ content: payload.content,
192
+ //content_blob: payload.content,
193
+ content_hash: payload.content_hash,
194
+ metadata_flags: payload.metadata_flags,
195
+ received_at: new Date().toISOString()
196
+ };
155
197
 
156
- this.log(`🚀 Sending Capture [${payload.turn_role}]`);
198
+ this.log(`🚀 Sending Capture [${payload.turn_role}]`);
157
199
 
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
- });
200
+ const req = https.request(this.captureEndpoint, {
201
+ method: 'POST',
202
+ headers: {
203
+ 'Content-Type': 'application/json',
204
+ 'x-dropout-key': this.apiKey
205
+ }
206
+ });
165
207
 
166
- req.on('error', (e) => this.log("❌ Upload Failed", e.message));
167
- req.write(JSON.stringify(finalPayload));
168
- req.end();
208
+ req.on('error', (e) => this.log("❌ Upload Failed (Silent)", e.message));
209
+ req.write(JSON.stringify(finalPayload));
210
+ req.end();
211
+ } catch (e) {
212
+ this.log("âš ī¸ Emit Error (Silent)", e);
213
+ }
169
214
  }
170
215
 
171
216
  // --- CORE PATCHING ---
@@ -176,14 +221,14 @@ class Dropout {
176
221
  if (global.fetch) {
177
222
  const originalFetch = global.fetch;
178
223
  global.fetch = async function (input, init) {
179
- const url = typeof input === 'string' ? input : input?.url;
224
+ let url;
225
+ try { url = typeof input === 'string' ? input : input?.url; } catch (e) { return originalFetch.apply(this, arguments); }
180
226
 
181
227
  let bodyStr = "";
182
228
  if (init && init.body) {
183
229
  try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
184
230
  }
185
231
 
186
- // 1. Guard
187
232
  if (!_this.isAiRequest(url, bodyStr)) {
188
233
  return originalFetch.apply(this, arguments);
189
234
  }
@@ -191,7 +236,6 @@ class Dropout {
191
236
  _this.log(`⚡ [FETCH] Intercepting: ${url}`);
192
237
  const start = Date.now();
193
238
 
194
- // 2. Determine Turn Logic (Your Snippet)
195
239
  let activeTurn;
196
240
  if (_this.lastTurnConfirmed) {
197
241
  activeTurn = _this.turnIndex++;
@@ -203,7 +247,6 @@ class Dropout {
203
247
  const { provider, model } = _this.normalize(url, bodyStr);
204
248
  const pHash = _this.hash(bodyStr);
205
249
 
206
- // 3. Emit Request (User)
207
250
  _this.emit({
208
251
  session_id: global.__dropout_session_id__,
209
252
  turn_index: activeTurn,
@@ -212,15 +255,10 @@ class Dropout {
212
255
  model,
213
256
  content: _this.privacy === 'full' ? bodyStr : null,
214
257
  content_hash: pHash,
215
- metadata_flags: {
216
- retry_like: pHash === _this.lastPromptHash ? 1 : 0,
217
- method: 'FETCH',
218
- url: url
219
- }
258
+ metadata_flags: { retry_like: pHash === _this.lastPromptHash ? 1 : 0, method: 'FETCH', url: url }
220
259
  });
221
260
  _this.lastPromptHash = pHash;
222
261
 
223
- // 4. Actual Network Call
224
262
  let response;
225
263
  try {
226
264
  response = await originalFetch.apply(this, arguments);
@@ -228,13 +266,10 @@ class Dropout {
228
266
 
229
267
  const latency = Date.now() - start;
230
268
 
231
- // 5. Emit Response (Assistant)
232
269
  try {
233
270
  const cloned = response.clone();
234
271
  let oText = await cloned.text();
235
- if (oText && oText.length > _this.maxOutputBytes) {
236
- oText = oText.slice(0, _this.maxOutputBytes);
237
- }
272
+ if (oText && oText.length > _this.maxOutputBytes) oText = oText.slice(0, _this.maxOutputBytes);
238
273
  const oHash = _this.hash(oText);
239
274
 
240
275
  _this.emit({
@@ -266,15 +301,17 @@ class Dropout {
266
301
  const originalRequest = module.request;
267
302
  module.request = function (...args) {
268
303
  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
- }
304
+ try {
305
+ if (typeof args[0] === 'string') {
306
+ url = args[0];
307
+ } else {
308
+ const opts = args[0] || {};
309
+ const protocol = opts.protocol || (moduleName === 'https' ? 'https:' : 'http:');
310
+ const host = opts.hostname || opts.host || 'localhost';
311
+ const path = opts.path || '/';
312
+ url = `${protocol}//${host}${path}`;
313
+ }
314
+ } catch (e) { return originalRequest.apply(this, args); }
278
315
 
279
316
  if (!_this.isAiRequest(url, null)) {
280
317
  return originalRequest.apply(this, args);
@@ -288,88 +325,81 @@ class Dropout {
288
325
  const originalWrite = clientRequest.write;
289
326
  const originalEnd = clientRequest.end;
290
327
 
291
- // Buffer Request Data
292
328
  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
- }
329
+ try {
330
+ const chunk = writeArgs[0];
331
+ if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
332
+ reqChunks.push(Buffer.from(chunk));
333
+ }
334
+ } catch (e) { }
297
335
  return originalWrite.apply(this, writeArgs);
298
336
  };
299
337
 
300
338
  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
- }
339
+ try {
340
+ const chunk = endArgs[0];
341
+ if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
342
+ reqChunks.push(Buffer.from(chunk));
343
+ }
305
344
 
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);
345
+ const reqBody = Buffer.concat(reqChunks).toString('utf8');
346
+ const { provider, model } = _this.normalize(url, reqBody);
347
+ const pHash = _this.hash(reqBody);
310
348
 
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
- }
349
+ let activeTurn;
350
+ if (_this.lastTurnConfirmed) {
351
+ activeTurn = _this.turnIndex++;
352
+ _this.lastTurnConfirmed = false;
353
+ } else {
354
+ activeTurn = _this.turnIndex > 0 ? _this.turnIndex - 1 : 0;
355
+ }
319
356
 
320
- // Save state for response
321
- clientRequest._dropout_turn = activeTurn;
322
- clientRequest._dropout_provider = provider;
323
- clientRequest._dropout_model = model;
357
+ clientRequest._dropout_turn = activeTurn;
358
+ clientRequest._dropout_provider = provider;
359
+ clientRequest._dropout_model = model;
324
360
 
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;
361
+ _this.emit({
362
+ session_id: global.__dropout_session_id__,
363
+ turn_index: activeTurn,
364
+ turn_role: 'user',
365
+ provider,
366
+ model,
367
+ content: _this.privacy === 'full' ? reqBody : null,
368
+ content_hash: pHash,
369
+ metadata_flags: { retry_like: pHash === _this.lastPromptHash ? 1 : 0, method: moduleName.toUpperCase(), url: url }
370
+ });
371
+ _this.lastPromptHash = pHash;
372
+ } catch (e) { _this.log("âš ī¸ Request Capture Failed"); }
340
373
 
341
374
  return originalEnd.apply(this, endArgs);
342
375
  };
343
376
 
344
- // --- EMIT RESPONSE ---
345
377
  clientRequest.on('response', (res) => {
346
378
  const resChunks = [];
347
379
  res.on('data', (chunk) => resChunks.push(chunk));
348
380
  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;
381
+ try {
382
+ let resBody = Buffer.concat(resChunks).toString('utf8');
383
+ if (resBody && resBody.length > _this.maxOutputBytes) resBody = resBody.slice(0, _this.maxOutputBytes);
384
+
385
+ const latency = Date.now() - start;
386
+ const oHash = _this.hash(resBody);
387
+
388
+ _this.emit({
389
+ session_id: global.__dropout_session_id__,
390
+ turn_index: clientRequest._dropout_turn || 0,
391
+ turn_role: 'assistant',
392
+ latency_ms: latency,
393
+ provider: clientRequest._dropout_provider,
394
+ model: clientRequest._dropout_model,
395
+ content: _this.privacy === 'full' ? resBody : null,
396
+ content_hash: oHash,
397
+ metadata_flags: { status: res.statusCode, non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0 }
398
+ });
399
+
400
+ _this.lastResponseHash = oHash;
401
+ _this.lastTurnConfirmed = true;
402
+ } catch (e) { _this.log("âš ī¸ Response Capture Failed"); }
373
403
  });
374
404
  });
375
405
 
@@ -383,8 +413,8 @@ class Dropout {
383
413
  }
384
414
  }
385
415
 
386
- // Auto-Start (Node Preload)
387
- if (process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
416
+ // Auto-Start (Server-Side Only)
417
+ if (typeof process !== 'undefined' && process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
388
418
  new Dropout({
389
419
  projectId: process.env.DROPOUT_PROJECT_ID,
390
420
  apiKey: process.env.DROPOUT_API_KEY,