@dropout-ai/runtime 0.3.2 → 0.3.3
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 +318 -179
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dropout-ai/runtime
|
|
3
3
|
* Universal AI Interaction Capture for Node.js & Next.js
|
|
4
|
-
*
|
|
4
|
+
* Role: Passive observer.
|
|
5
|
+
* Behavior: Fire-and-forget raw JSON to Supabase.
|
|
5
6
|
* Capability: Captures Genkit, OpenAI, LangChain, Axios, and standard fetch.
|
|
6
7
|
*/
|
|
7
8
|
|
|
@@ -9,20 +10,39 @@ const https = require('https');
|
|
|
9
10
|
const http = require('http');
|
|
10
11
|
const crypto = require('crypto');
|
|
11
12
|
|
|
12
|
-
// --- CONFIGURATION ---
|
|
13
|
+
// --- DEFAULT CONFIGURATION ---
|
|
14
|
+
const SUPABASE_FUNCTION_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
|
|
15
|
+
|
|
16
|
+
// Known AI Domains for Auto-Detection
|
|
13
17
|
const KNOWN_AI_DOMAINS = [
|
|
14
|
-
'api.openai.com',
|
|
15
|
-
'api.
|
|
16
|
-
'generativelanguage.googleapis.com', // Gemini
|
|
17
|
-
'aiplatform.googleapis.com', // Vertex AI (Genkit often uses this)
|
|
18
|
-
'api.groq.com', // Groq
|
|
19
|
-
'api.mistral.ai', // Mistral
|
|
20
|
-
'api.cohere.ai' // Cohere
|
|
18
|
+
'api.openai.com', 'api.anthropic.com', 'generativelanguage.googleapis.com',
|
|
19
|
+
'aiplatform.googleapis.com', 'api.groq.com', 'api.mistral.ai', 'api.cohere.ai'
|
|
21
20
|
];
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
// Global State
|
|
23
|
+
const GLOBAL_OBJ = typeof window !== 'undefined' ? window : global;
|
|
24
|
+
let activeConfig = {
|
|
25
|
+
projectId: null,
|
|
26
|
+
apiKey: null,
|
|
27
|
+
captureEndpoint: SUPABASE_FUNCTION_URL,
|
|
28
|
+
maxOutputBytes: 32768,
|
|
29
|
+
privacyMode: (typeof process !== 'undefined' && process.env.DROPOUT_PRIVACY_MODE) || 'full',
|
|
30
|
+
debug: false // Added debug flag
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let isPatched = false;
|
|
34
|
+
let turnIndex = 0;
|
|
35
|
+
let lastTurnConfirmed = true;
|
|
36
|
+
let lastPromptHash = null;
|
|
37
|
+
let lastResponseHash = null;
|
|
38
|
+
let instance = null;
|
|
24
39
|
|
|
25
40
|
// --- UTILS ---
|
|
41
|
+
|
|
42
|
+
function log(msg, ...args) {
|
|
43
|
+
if (activeConfig.debug) console.log(`[Dropout] ${msg}`, ...args);
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
function generateSessionId() {
|
|
27
47
|
try {
|
|
28
48
|
return crypto.randomUUID();
|
|
@@ -31,210 +51,329 @@ function generateSessionId() {
|
|
|
31
51
|
}
|
|
32
52
|
}
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
54
|
+
function hash(text) {
|
|
55
|
+
if (!text) return null;
|
|
56
|
+
try {
|
|
57
|
+
return crypto.createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
|
|
58
|
+
} catch (e) { return 'hash_err'; }
|
|
59
|
+
}
|
|
41
60
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
function normalize(url, body) {
|
|
62
|
+
let provider = 'custom';
|
|
63
|
+
let model = 'unknown';
|
|
64
|
+
|
|
65
|
+
if (url) {
|
|
66
|
+
const u = url.toLowerCase();
|
|
67
|
+
if (u.includes('openai')) provider = 'openai';
|
|
68
|
+
else if (u.includes('anthropic')) provider = 'anthropic';
|
|
69
|
+
else if (u.includes('google') || u.includes('generative') || u.includes('aiplatform')) provider = 'google';
|
|
70
|
+
else if (u.includes('groq')) provider = 'groq';
|
|
71
|
+
else if (u.includes('mistral')) provider = 'mistral';
|
|
72
|
+
else if (u.includes('cohere')) provider = 'cohere';
|
|
73
|
+
else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
|
|
74
|
+
}
|
|
48
75
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
76
|
+
if (body) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
|
|
79
|
+
if (parsed.model) model = parsed.model;
|
|
80
|
+
} catch (e) { }
|
|
81
|
+
}
|
|
82
|
+
return { provider, model };
|
|
83
|
+
}
|
|
55
84
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
function isAiRequest(url, bodyString) {
|
|
86
|
+
if (!url) return false;
|
|
87
|
+
// Guard: Infinite Loop Prevention
|
|
88
|
+
if (url.includes(activeConfig.captureEndpoint)) return false;
|
|
60
89
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.patchNetwork();
|
|
64
|
-
}
|
|
90
|
+
// 1. Check Known Domains
|
|
91
|
+
if (KNOWN_AI_DOMAINS.some(d => url.includes(d))) return true;
|
|
65
92
|
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
// 2. Heuristic Check (for custom endpoints)
|
|
94
|
+
if (bodyString) {
|
|
95
|
+
return (bodyString.includes('"model"') || bodyString.includes('"messages"')) &&
|
|
96
|
+
(bodyString.includes('"user"') || bodyString.includes('"prompt"'));
|
|
68
97
|
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
69
100
|
|
|
70
|
-
|
|
71
|
-
if (!url) return false;
|
|
72
|
-
// Guard: Infinite Loop Prevention (Don't capture our own calls)
|
|
73
|
-
if (url.includes(DROPOUT_INGEST_URL)) return false;
|
|
101
|
+
// --- EMITTER (Authenticated) ---
|
|
74
102
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
103
|
+
function emit(payload) {
|
|
104
|
+
// 1. Guard: Do not emit if not initialized
|
|
105
|
+
if (!activeConfig.apiKey || !activeConfig.projectId) return;
|
|
78
106
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
// 2. Construct Final Payload
|
|
108
|
+
const finalPayload = {
|
|
109
|
+
...payload,
|
|
110
|
+
project_id: activeConfig.projectId,
|
|
82
111
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const url = typeof input === 'string' ? input : input?.url;
|
|
112
|
+
// ✅ Ensure these columns are always populated for the DB
|
|
113
|
+
content_blob: payload.content, // Map content to blob
|
|
114
|
+
received_at: new Date().toISOString()
|
|
115
|
+
};
|
|
88
116
|
|
|
89
|
-
|
|
90
|
-
if (!_this.isAiRequest(url)) {
|
|
91
|
-
return originalFetch.apply(this, arguments);
|
|
92
|
-
}
|
|
117
|
+
log(`🚀 Sending Capture (${payload.turn_role})`);
|
|
93
118
|
|
|
94
|
-
|
|
119
|
+
// 3. Fire and Forget using HTTPS (Bypassing our own patches)
|
|
120
|
+
const req = https.request(activeConfig.captureEndpoint, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
'x-dropout-key': activeConfig.apiKey
|
|
125
|
+
}
|
|
126
|
+
});
|
|
95
127
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
128
|
+
req.on('error', (e) => log("❌ Upload Failed", e.message));
|
|
129
|
+
req.write(JSON.stringify(finalPayload));
|
|
130
|
+
req.end();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- MONKEY PATCH ---
|
|
99
134
|
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
function applyPatch() {
|
|
136
|
+
if (isPatched) return;
|
|
102
137
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
// --- A. PATCH FETCH (Next.js / OpenAI) ---
|
|
139
|
+
if (GLOBAL_OBJ.fetch) {
|
|
140
|
+
GLOBAL_OBJ.__dropout_original_fetch__ = GLOBAL_OBJ.fetch;
|
|
141
|
+
GLOBAL_OBJ.fetch = async function (input, init) {
|
|
142
|
+
const url = typeof input === 'string' ? input : (input && input.url);
|
|
107
143
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
// Check body string safely
|
|
145
|
+
let bodyStr = "";
|
|
146
|
+
if (init && init.body) {
|
|
147
|
+
try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
148
|
+
}
|
|
112
149
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
150
|
+
// Guard
|
|
151
|
+
if (!isAiRequest(url, bodyStr)) {
|
|
152
|
+
return GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log(`⚡ [FETCH] Intercepting: ${url}`);
|
|
156
|
+
|
|
157
|
+
const start = Date.now();
|
|
158
|
+
|
|
159
|
+
// Calculate Turn
|
|
160
|
+
let activeTurn;
|
|
161
|
+
if (lastTurnConfirmed) {
|
|
162
|
+
activeTurn = turnIndex++;
|
|
163
|
+
lastTurnConfirmed = false;
|
|
164
|
+
} else {
|
|
165
|
+
activeTurn = turnIndex > 0 ? turnIndex - 1 : 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { provider, model } = normalize(url, bodyStr);
|
|
169
|
+
const pHash = hash(bodyStr);
|
|
170
|
+
|
|
171
|
+
// Emit Request (User)
|
|
172
|
+
emit({
|
|
173
|
+
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
174
|
+
turn_index: activeTurn,
|
|
175
|
+
direction: 'user_to_ai',
|
|
176
|
+
turn_role: 'user',
|
|
177
|
+
provider,
|
|
178
|
+
model,
|
|
179
|
+
content: activeConfig.privacyMode === 'full' ? bodyStr : null,
|
|
180
|
+
content_hash: pHash,
|
|
181
|
+
metadata_flags: {
|
|
182
|
+
retry_like: pHash === lastPromptHash ? 1 : 0,
|
|
183
|
+
method: 'FETCH',
|
|
184
|
+
url: url
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
lastPromptHash = pHash;
|
|
188
|
+
|
|
189
|
+
// Actual Call
|
|
190
|
+
let response;
|
|
191
|
+
try {
|
|
192
|
+
response = await GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
193
|
+
} catch (err) { throw err; }
|
|
194
|
+
|
|
195
|
+
const latency = Date.now() - start;
|
|
196
|
+
|
|
197
|
+
// Emit Response (Assistant)
|
|
198
|
+
try {
|
|
199
|
+
const cloned = response.clone();
|
|
200
|
+
const oText = await cloned.text();
|
|
201
|
+
const oHash = hash(oText);
|
|
202
|
+
|
|
203
|
+
emit({
|
|
204
|
+
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
205
|
+
turn_index: activeTurn,
|
|
206
|
+
direction: 'ai_to_user',
|
|
207
|
+
turn_role: 'assistant',
|
|
208
|
+
latency_ms: latency,
|
|
209
|
+
provider,
|
|
210
|
+
model,
|
|
211
|
+
content: activeConfig.privacyMode === 'full' ? oText : null,
|
|
212
|
+
content_hash: oHash,
|
|
213
|
+
metadata_flags: {
|
|
214
|
+
non_adaptive_response: oHash === lastResponseHash ? 1 : 0,
|
|
215
|
+
turn_boundary_confirmed: 1,
|
|
119
216
|
status: response.status
|
|
120
|
-
}
|
|
121
|
-
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
lastResponseHash = oHash;
|
|
220
|
+
} catch (e) { log("⚠️ Failed to read response body"); }
|
|
122
221
|
|
|
123
|
-
|
|
222
|
+
lastTurnConfirmed = true;
|
|
223
|
+
return response;
|
|
224
|
+
};
|
|
225
|
+
log("✅ Patch Applied: global.fetch");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- B. PATCH HTTP/HTTPS (Axios / Genkit / Node SDKs) ---
|
|
229
|
+
const patchNodeRequest = (module, moduleName) => {
|
|
230
|
+
const originalRequest = module.request;
|
|
231
|
+
module.request = function (...args) {
|
|
232
|
+
// Resolve URL
|
|
233
|
+
let url;
|
|
234
|
+
let options;
|
|
235
|
+
if (typeof args[0] === 'string') {
|
|
236
|
+
url = args[0];
|
|
237
|
+
options = args[1] || {};
|
|
238
|
+
} else {
|
|
239
|
+
options = args[0] || {};
|
|
240
|
+
const protocol = options.protocol || 'https:';
|
|
241
|
+
const host = options.hostname || options.host || 'localhost';
|
|
242
|
+
const path = options.path || '/';
|
|
243
|
+
url = `${protocol}//${host}${path}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// We can't check body here yet, so we rely on URL
|
|
247
|
+
if (!isAiRequest(url, null)) {
|
|
248
|
+
return originalRequest.apply(this, args);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
log(`⚡ [${moduleName.toUpperCase()}] Intercepting: ${url}`);
|
|
252
|
+
|
|
253
|
+
const start = Date.now();
|
|
254
|
+
const clientRequest = originalRequest.apply(this, args);
|
|
255
|
+
|
|
256
|
+
// Capture Request Body
|
|
257
|
+
const reqChunks = [];
|
|
258
|
+
const originalWrite = clientRequest.write;
|
|
259
|
+
const originalEnd = clientRequest.end;
|
|
260
|
+
|
|
261
|
+
clientRequest.write = function (...writeArgs) {
|
|
262
|
+
if (writeArgs[0]) reqChunks.push(Buffer.from(writeArgs[0]));
|
|
263
|
+
return originalWrite.apply(this, writeArgs);
|
|
124
264
|
};
|
|
125
|
-
this.log("✅ Patch Applied: global.fetch");
|
|
126
|
-
}
|
|
127
265
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
266
|
+
clientRequest.end = function (...endArgs) {
|
|
267
|
+
if (endArgs[0]) reqChunks.push(Buffer.from(endArgs[0]));
|
|
268
|
+
|
|
269
|
+
// Request Complete - Emit User Turn
|
|
270
|
+
const reqBody = Buffer.concat(reqChunks).toString('utf8');
|
|
271
|
+
const { provider, model } = normalize(url, reqBody);
|
|
272
|
+
const pHash = hash(reqBody);
|
|
273
|
+
|
|
274
|
+
let activeTurn;
|
|
275
|
+
if (lastTurnConfirmed) {
|
|
276
|
+
activeTurn = turnIndex++;
|
|
277
|
+
lastTurnConfirmed = false;
|
|
138
278
|
} else {
|
|
139
|
-
|
|
140
|
-
const protocol = options.protocol || 'https:';
|
|
141
|
-
const host = options.hostname || options.host || 'localhost';
|
|
142
|
-
const path = options.path || '/';
|
|
143
|
-
url = `${protocol}//${host}${path}`;
|
|
279
|
+
activeTurn = turnIndex > 0 ? turnIndex - 1 : 0;
|
|
144
280
|
}
|
|
145
281
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
282
|
+
// Save state on request object for response handler
|
|
283
|
+
clientRequest._dropout_turn = activeTurn;
|
|
284
|
+
clientRequest._dropout_provider = provider;
|
|
285
|
+
clientRequest._dropout_model = model;
|
|
286
|
+
|
|
287
|
+
emit({
|
|
288
|
+
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
289
|
+
turn_index: activeTurn,
|
|
290
|
+
direction: 'user_to_ai',
|
|
291
|
+
turn_role: 'user',
|
|
292
|
+
provider,
|
|
293
|
+
model,
|
|
294
|
+
content: activeConfig.privacyMode === 'full' ? reqBody : null,
|
|
295
|
+
content_hash: pHash,
|
|
296
|
+
metadata_flags: { method: moduleName.toUpperCase(), url: url }
|
|
297
|
+
});
|
|
298
|
+
lastPromptHash = pHash;
|
|
299
|
+
|
|
300
|
+
return originalEnd.apply(this, endArgs);
|
|
301
|
+
};
|
|
150
302
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// Capture Response
|
|
174
|
-
clientRequest.on('response', (res) => {
|
|
175
|
-
const resChunks = [];
|
|
176
|
-
res.on('data', (chunk) => resChunks.push(chunk));
|
|
177
|
-
res.on('end', () => {
|
|
178
|
-
const resBody = Buffer.concat(resChunks).toString('utf8');
|
|
179
|
-
_this.emit({
|
|
180
|
-
url,
|
|
181
|
-
method: moduleName.toUpperCase(),
|
|
182
|
-
request: clientRequest._fullBody,
|
|
183
|
-
response: resBody,
|
|
184
|
-
latency: Date.now() - startTime,
|
|
185
|
-
status: res.statusCode
|
|
186
|
-
});
|
|
303
|
+
// Capture Response
|
|
304
|
+
clientRequest.on('response', (res) => {
|
|
305
|
+
const resChunks = [];
|
|
306
|
+
res.on('data', (chunk) => resChunks.push(chunk));
|
|
307
|
+
res.on('end', () => {
|
|
308
|
+
const resBody = Buffer.concat(resChunks).toString('utf8');
|
|
309
|
+
const latency = Date.now() - start;
|
|
310
|
+
const oHash = hash(resBody);
|
|
311
|
+
|
|
312
|
+
emit({
|
|
313
|
+
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
314
|
+
turn_index: clientRequest._dropout_turn || 0,
|
|
315
|
+
direction: 'ai_to_user',
|
|
316
|
+
turn_role: 'assistant',
|
|
317
|
+
latency_ms: latency,
|
|
318
|
+
provider: clientRequest._dropout_provider,
|
|
319
|
+
model: clientRequest._dropout_model,
|
|
320
|
+
content: activeConfig.privacyMode === 'full' ? resBody : null,
|
|
321
|
+
content_hash: oHash,
|
|
322
|
+
metadata_flags: { status: res.statusCode }
|
|
187
323
|
});
|
|
324
|
+
|
|
325
|
+
lastResponseHash = oHash;
|
|
326
|
+
lastTurnConfirmed = true;
|
|
188
327
|
});
|
|
328
|
+
});
|
|
189
329
|
|
|
190
|
-
|
|
191
|
-
};
|
|
330
|
+
return clientRequest;
|
|
192
331
|
};
|
|
332
|
+
};
|
|
193
333
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
334
|
+
patchNodeRequest(https, 'https');
|
|
335
|
+
patchNodeRequest(http, 'http');
|
|
336
|
+
log("✅ Patch Applied: http/https");
|
|
198
337
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// 1. Construct Payload
|
|
202
|
-
// We combine Request + Response into a single content blob for simplicity in this universal mode
|
|
203
|
-
const content = `--- REQUEST ---\n${data.request}\n\n--- RESPONSE ---\n${data.response}`;
|
|
204
|
-
|
|
205
|
-
const payload = {
|
|
206
|
-
project_id: this.projectId,
|
|
207
|
-
session_id: global.__dropout_session_id__,
|
|
208
|
-
turn_role: 'assistant', // Default to assistant for system-captured logs
|
|
209
|
-
turn_index: 0, // In universal mode, we capture raw streams
|
|
210
|
-
//content_blob: this.privacy === 'full' ? content : null,
|
|
211
|
-
content: this.privacy === 'full' ? content : null,
|
|
212
|
-
metadata_flags: {
|
|
213
|
-
latency: data.latency,
|
|
214
|
-
url: data.url,
|
|
215
|
-
method: data.method,
|
|
216
|
-
status: data.status,
|
|
217
|
-
captured_via: 'universal_interceptor'
|
|
218
|
-
},
|
|
219
|
-
received_at: new Date().toISOString()
|
|
220
|
-
};
|
|
338
|
+
isPatched = true;
|
|
339
|
+
}
|
|
221
340
|
|
|
222
|
-
|
|
341
|
+
// --- MAIN CLASS EXPORT ---
|
|
223
342
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
method: 'POST',
|
|
228
|
-
headers: {
|
|
229
|
-
'Content-Type': 'application/json',
|
|
230
|
-
'x-dropout-key': this.apiKey
|
|
231
|
-
}
|
|
232
|
-
});
|
|
343
|
+
class Dropout {
|
|
344
|
+
constructor(config = {}) {
|
|
345
|
+
if (instance) return instance;
|
|
233
346
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
347
|
+
if (!config.apiKey || !config.projectId) {
|
|
348
|
+
console.warn("[Dropout] Missing apiKey or projectId. Tracking disabled.");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Update Global Configuration
|
|
353
|
+
activeConfig.apiKey = config.apiKey;
|
|
354
|
+
activeConfig.projectId = config.projectId;
|
|
355
|
+
if (config.privacyMode) activeConfig.privacyMode = config.privacyMode;
|
|
356
|
+
activeConfig.debug = config.debug || false;
|
|
357
|
+
|
|
358
|
+
// Initialize Identity
|
|
359
|
+
if (!GLOBAL_OBJ.__dropout_session_id__) {
|
|
360
|
+
GLOBAL_OBJ.__dropout_session_id__ = generateSessionId();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Apply the patch
|
|
364
|
+
if (activeConfig.debug) console.log(`[Dropout] 🟢 Initialized for Project: ${config.projectId}`);
|
|
365
|
+
applyPatch();
|
|
366
|
+
instance = this;
|
|
237
367
|
}
|
|
238
368
|
}
|
|
239
369
|
|
|
370
|
+
// Auto-Start
|
|
371
|
+
if (process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
|
|
372
|
+
new Dropout({
|
|
373
|
+
projectId: process.env.DROPOUT_PROJECT_ID,
|
|
374
|
+
apiKey: process.env.DROPOUT_API_KEY,
|
|
375
|
+
debug: process.env.DROPOUT_DEBUG === 'true'
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
240
379
|
module.exports = Dropout;
|