@lognroll/lib 1.0.0

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.
@@ -0,0 +1,383 @@
1
+ import Core from "../core";
2
+
3
+ export class Network extends Core {
4
+ constructor() {
5
+ super();
6
+ this.wrap();
7
+ }
8
+ private responseLogs: any[] = [];
9
+ private processedResources = new Set<string>();
10
+ private resourceMonitorInterval: number | null = null;
11
+
12
+ getResponseLogs() {
13
+ return [...this.responseLogs];
14
+ }
15
+
16
+ clearResponseLogs() {
17
+ this.responseLogs = [];
18
+ this.processedResources.clear();
19
+ }
20
+
21
+ private monitorResourceTiming() {
22
+ if (typeof window === 'undefined' || !window.performance || !window.performance.getEntriesByType) {
23
+ return;
24
+ }
25
+
26
+ const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
27
+
28
+ resources.forEach(resource => {
29
+ // Skip if already processed or if it's a lognroll request
30
+ if (this.processedResources.has(resource.name) || resource.name.includes('lognroll')) {
31
+ return;
32
+ }
33
+
34
+ // Add to processed set
35
+ this.processedResources.add(resource.name);
36
+
37
+ // Calculate timing details
38
+ const timing = {
39
+ startTime: resource.startTime,
40
+ duration: resource.duration,
41
+ fetchStart: resource.fetchStart,
42
+ responseEnd: resource.responseEnd,
43
+ // DNS lookup time
44
+ dnsTime: resource.domainLookupEnd - resource.domainLookupStart,
45
+ // Connection time
46
+ connectTime: resource.connectEnd - resource.connectStart,
47
+ // Time to First Byte (TTFB)
48
+ ttfb: resource.responseStart - resource.requestStart,
49
+ // Content download time
50
+ downloadTime: resource.responseEnd - resource.responseStart
51
+ };
52
+
53
+ // console.log('resource', resource)
54
+ // Create response log entry
55
+ const responseLog = {
56
+ type: 'network',
57
+ stage: 'response',
58
+ payload: {
59
+ request: {
60
+ id: `resource-${resource.startTime}`,
61
+ url: resource.name,
62
+ timestamp: Date.now() - resource.duration,
63
+ method: 'GET',
64
+ initiatorType: resource.initiatorType,
65
+ body: null
66
+ },
67
+ response: {
68
+ url: resource.name,
69
+ status: 200, // Resources that fail to load won't appear in the performance entries
70
+ statusText: 'OK',
71
+ headers: {},
72
+ timing,
73
+ contentType: resource.initiatorType,
74
+ decodedBodySize: resource.decodedBodySize,
75
+ encodedBodySize: resource.encodedBodySize,
76
+ transferSize: resource.transferSize,
77
+ timestamp: Date.now(),
78
+ responseTime: resource.duration
79
+ }
80
+ }
81
+ };
82
+
83
+ this.responseLogs.push(responseLog);
84
+ });
85
+
86
+ // Clear the performance buffer to avoid processing the same resources again
87
+ performance.clearResourceTimings();
88
+ }
89
+
90
+ startResourceMonitoring(interval: number = 1000) {
91
+ if (this.resourceMonitorInterval !== null) {
92
+ return;
93
+ }
94
+
95
+ // Initial check
96
+ this.monitorResourceTiming();
97
+
98
+ // Set up periodic monitoring
99
+ this.resourceMonitorInterval = window.setInterval(() => {
100
+ this.monitorResourceTiming();
101
+ }, interval);
102
+ }
103
+
104
+ stopResourceMonitoring() {
105
+ if (this.resourceMonitorInterval !== null) {
106
+ clearInterval(this.resourceMonitorInterval);
107
+ this.resourceMonitorInterval = null;
108
+ }
109
+ }
110
+
111
+ wrap() {
112
+ // Start resource monitoring
113
+ this.startResourceMonitoring();
114
+
115
+ // Store for tracking requests
116
+ const requestStore = new Map<string, { url: string; timestamp: number, body: any }>();
117
+
118
+ // Wrap fetch
119
+ const originalFetch = window.fetch;
120
+ const _this = this;
121
+ window.fetch = async function (input: RequestInfo | URL, init?: RequestInit) {
122
+ const requestId = Date.now().toString();
123
+ const url = typeof input === 'string' ? input : (input as Request).url;
124
+
125
+ // Skip requests containing 'lognroll' in the URL
126
+ if (url.includes('lognroll')) {
127
+ return originalFetch(input, init);
128
+ }
129
+
130
+ const method = init?.method || 'GET';
131
+ const body = init?.body || null;
132
+
133
+ requestStore.set(requestId, {url, timestamp: Date.now(), body: null});
134
+
135
+ // Execute original fetch
136
+ const startTime = Date.now();
137
+ const requestDetails = requestStore.get(requestId);
138
+ try {
139
+ const response = await originalFetch(input, init);
140
+
141
+ // Clone response to read body
142
+ const clonedResponse = response.clone();
143
+ const responseData = await clonedResponse.text().catch(() => null);
144
+ // Log response
145
+ const responseTime = Date.now() - startTime;
146
+ const responseLog = {
147
+ type: 'network',
148
+ stage: 'response',
149
+ payload: {
150
+ request: {
151
+ id: requestId,
152
+ url: response.url,
153
+ timestamp: requestDetails?.timestamp || Date.now() - 100,
154
+ method,
155
+ body
156
+ },
157
+ response: {
158
+ url: response.url,
159
+ status: response.status,
160
+ statusText: response.statusText,
161
+ headers: (() => {
162
+ const headersObj: Record<string, string> = {};
163
+ response.headers.forEach((value, key) => {
164
+ headersObj[key] = value;
165
+ });
166
+ return headersObj;
167
+ })(),
168
+ data: responseData,
169
+ timestamp: Date.now(),
170
+ responseTime
171
+ }
172
+ }
173
+ };
174
+
175
+ console.log('responseLog', responseLog);
176
+ _this.responseLogs.push(responseLog);
177
+ requestStore.delete(requestId);
178
+
179
+ return response;
180
+ } catch (error: unknown) {
181
+ // Log error
182
+ const errorLog = {
183
+ type: 'network',
184
+ stage: 'error',
185
+ payload: {
186
+ request: {
187
+ id: requestId,
188
+ url,
189
+ timestamp: requestDetails?.timestamp || Date.now() - 100,
190
+ method,
191
+ body
192
+ },
193
+ error: {
194
+ message: error instanceof Error ? error.message : 'Unknown error',
195
+ timestamp: Date.now()
196
+ }
197
+ }
198
+ };
199
+
200
+ // console.error(JSON.stringify(errorLog));
201
+ _this.responseLogs.push(errorLog);
202
+ requestStore.delete(requestId);
203
+ throw error;
204
+ }
205
+ };
206
+
207
+ // Wrap XMLHttpRequest
208
+ const originalXHR = window.XMLHttpRequest;
209
+ // @ts-ignore - We're intentionally replacing the constructor
210
+ window.XMLHttpRequest = function () {
211
+ const xhr = new originalXHR();
212
+ const requestId = Date.now().toString();
213
+
214
+ const originalOpen = xhr.open;
215
+
216
+ // Store custom headers for this specific XHR instance
217
+ (xhr as any)._requestHeaders = {};
218
+
219
+ // Preserve the original setRequestHeader for this instance
220
+ const originalSetRequestHeader = xhr.setRequestHeader;
221
+ xhr.setRequestHeader = function(header, value) {
222
+ (xhr as any)._requestHeaders[header] = value;
223
+ originalSetRequestHeader.apply(this, arguments as any);
224
+ };
225
+
226
+ xhr.open = function (method: string, url: string | URL) {
227
+ if (url.toString().includes('lognroll')) {
228
+ return originalOpen.apply(xhr, arguments as any);
229
+ }
230
+
231
+ requestStore.set(requestId, {url: url.toString(), timestamp: Date.now(), body: null});
232
+
233
+
234
+ // Store a reference to 'this' (the XHR object) to use in the modified onreadystatechange
235
+ const self = this;
236
+
237
+ // Preserve the original onreadystatechange handler if it exists
238
+ const originalOnReadyStateChange = this.onreadystatechange;
239
+
240
+ // Override onreadystatechange to intercept the response
241
+ this.onreadystatechange = function() {
242
+ if (self.readyState === 4) { // DONE state
243
+ // Log response details
244
+ console.log('XHR Response Received:');
245
+ console.log(' URL:', self.responseURL);
246
+ console.log(' Status:', self.status);
247
+ console.log(' Status Text:', self.statusText);
248
+ console.log(' Response Data:', self.responseText); // Or self.response, self.responseXML, etc.
249
+
250
+ // You can add additional logic here, like error handling or re-trying requests
251
+
252
+ const data = requestStore.get(requestId);
253
+ // Log response
254
+ const responseTime = Date.now() - (data?.timestamp || Date.now());
255
+ console.log('METHOD', method);
256
+ const responseLog = {
257
+ type: 'network',
258
+ stage: 'response',
259
+ payload: {
260
+ request: {
261
+ id: requestId,
262
+ url: url,
263
+ timestamp: requestStore.get(requestId)?.timestamp || Date.now() - 100,
264
+ method: method || 'GET',
265
+ body: data?.body || null,
266
+ headers: (xhr as any)._requestHeaders || {}
267
+ },
268
+ response: {
269
+ status: self.status,
270
+ statusText: self.statusText,
271
+ headers: self.getAllResponseHeaders().split('\r\n').reduce((acc: Record<string, string>, line: string) => {
272
+ const [key, value] = line.split(': ');
273
+ if (key && value) acc[key] = value;
274
+ return acc;
275
+ }, {}),
276
+ data: self.responseText,
277
+ timestamp: Date.now(),
278
+ responseTime
279
+ }
280
+ }
281
+ };
282
+
283
+ // console.log(JSON.stringify(responseLog));
284
+ console.log('responseLog', responseLog);
285
+
286
+ _this.responseLogs.push(responseLog);
287
+ requestStore.delete(requestId);
288
+
289
+ // Call the original onreadystatechange if it was defined
290
+ if (originalOnReadyStateChange) {
291
+ originalOnReadyStateChange.apply(self, arguments as any);
292
+ }
293
+ } else {
294
+ // If there was an original onreadystatechange, call it for other states
295
+ if (originalOnReadyStateChange) {
296
+ originalOnReadyStateChange.apply(self, arguments as any);
297
+ }
298
+ }
299
+ };
300
+
301
+
302
+ return originalOpen.apply(xhr, arguments as any);
303
+ };
304
+
305
+ const originalSend = xhr.send;
306
+ xhr.send = function (body: any) {
307
+ if (body instanceof FormData) {
308
+ console.log("FormData entries:");
309
+ const reqData: any = {};
310
+ for (const [key, value] of (body as any).entries()) {
311
+ console.log(`${key}:`, value);
312
+ reqData[key] = value;
313
+ }
314
+ body = reqData;
315
+ const data = requestStore.get(requestId);
316
+ if (data != null) {
317
+ data['body'] = reqData;
318
+ }
319
+
320
+ } else {
321
+ const data = requestStore.get(requestId);
322
+ if (data != null) {
323
+ data['body'] = body;
324
+ }
325
+ console.log("Non-FormData body:", body);
326
+ }
327
+
328
+ xhr.addEventListener('load', function (progressEvent) {
329
+
330
+ const url = requestStore.get(requestId)?.url || '';
331
+ // Skip if URL contains 'lognroll'
332
+ if (url.includes('lognroll')) {
333
+ return;
334
+ }
335
+
336
+ if(url.includes('/api/v1/requests/')) {
337
+ // debugger;
338
+ }
339
+
340
+
341
+ console.log('url', url);
342
+ console.log('xhr.response', xhr.response, xhr);
343
+
344
+ });
345
+
346
+ xhr.addEventListener('error', function () {
347
+ // Skip if URL contains 'lognroll'
348
+ if (xhr.responseURL.includes('lognroll')) {
349
+ return;
350
+ }
351
+
352
+ // Log error
353
+ const errorLog = {
354
+ type: 'network',
355
+ stage: 'error',
356
+ payload: {
357
+ request: {
358
+ id: requestId,
359
+ url: xhr.responseURL,
360
+ timestamp: requestStore.get(requestId)?.timestamp || Date.now() - 100,
361
+ method: (xhr as any)._method || 'GET',
362
+ body
363
+ },
364
+ error: {
365
+ message: 'XHR request failed',
366
+ timestamp: Date.now()
367
+ }
368
+ }
369
+ };
370
+
371
+ _this.responseLogs.push(errorLog);
372
+ requestStore.delete(requestId);
373
+ });
374
+
375
+ return originalSend.apply(xhr, arguments as any);
376
+ };
377
+
378
+ return xhr;
379
+ };
380
+ }
381
+ }
382
+
383
+ export default Network;
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2019",
4
+ "module": "commonjs",
5
+ "outDir": "./dist",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "lib": ["es2019", "dom"]
9
+ },
10
+ "include": ["src"]
11
+ }
@@ -0,0 +1,30 @@
1
+ const webpack = require('webpack');
2
+ const path = require('path');
3
+
4
+ module.exports = {
5
+ entry: './src/index.js',
6
+ output: {
7
+ filename: 'bundle.js',
8
+ path: path.resolve(__dirname, 'dist')
9
+ },
10
+ plugins: [
11
+ new webpack.DefinePlugin({
12
+ PARAM_VALUE: JSON.stringify(process.env.PARAM_VALUE || 'defaultValue')
13
+ })
14
+ ],
15
+ mode: 'production',
16
+ module: {
17
+ rules: [
18
+ {
19
+ test: /\.js$/,
20
+ exclude: /node_modules|dist/,
21
+ use: {
22
+ loader: 'babel-loader',
23
+ options: {
24
+ presets: ['@babel/preset-env']
25
+ }
26
+ }
27
+ }
28
+ ]
29
+ }
30
+ };