console-bridge-sveltekit 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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/lib/client.d.ts +27 -0
- package/dist/lib/client.d.ts.map +1 -0
- package/dist/lib/client.js +364 -0
- package/dist/lib/server.d.ts +15 -0
- package/dist/lib/server.d.ts.map +1 -0
- package/dist/lib/server.js +61 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chintan Thakkar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# console-bridge-sveltekit
|
|
2
|
+
|
|
3
|
+
Forward frontend console logs to your SvelteKit backend for easier debugging. Perfect for AI agents and developers who want to see all logs in one place.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ Forward frontend console logs to backend
|
|
8
|
+
- ✅ See frontend + backend logs in single terminal
|
|
9
|
+
- ✅ Dev-only (zero production overhead)
|
|
10
|
+
- ✅ Batched requests for efficiency
|
|
11
|
+
- ✅ Recursive-safe
|
|
12
|
+
- ✅ Configurable log levels
|
|
13
|
+
- ✅ TypeScript support
|
|
14
|
+
- ✅ Stack trace capture for errors
|
|
15
|
+
- ✅ Capture fetch + XHR network calls
|
|
16
|
+
- ✅ Capture global errors/unhandled rejections
|
|
17
|
+
- ✅ Optional response body capture w limits
|
|
18
|
+
- ✅ URL include/ignore filters
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install console-bridge-sveltekit
|
|
24
|
+
# or
|
|
25
|
+
pnpm add console-bridge-sveltekit
|
|
26
|
+
# or
|
|
27
|
+
yarn add console-bridge-sveltekit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### 1. Create API Endpoint
|
|
33
|
+
|
|
34
|
+
Create `src/routes/api/console-bridge/+server.ts`:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { createConsoleBridgeEndpoint } from 'console-bridge-sveltekit/server';
|
|
38
|
+
|
|
39
|
+
export const POST = createConsoleBridgeEndpoint();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Initialize in Layout
|
|
43
|
+
|
|
44
|
+
In `src/routes/+layout.svelte`:
|
|
45
|
+
|
|
46
|
+
```svelte
|
|
47
|
+
<script>
|
|
48
|
+
import { onMount } from 'svelte';
|
|
49
|
+
import { initConsolebridge } from 'console-bridge-sveltekit/client';
|
|
50
|
+
|
|
51
|
+
onMount(() => {
|
|
52
|
+
initConsolebridge();
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
That's it! Now all frontend console logs will appear in your server terminal.
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
### Client Options
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
initConsolebridge({
|
|
65
|
+
endpoint: '/api/console-bridge', // API endpoint
|
|
66
|
+
batchSize: 10, // Logs per batch
|
|
67
|
+
batchDelay: 100, // Batch delay (ms)
|
|
68
|
+
levels: ['error', 'warn'], // Only forward errors and warnings
|
|
69
|
+
captureNetwork: true, // Capture fetch/XHR
|
|
70
|
+
captureErrors: true, // Capture global errors
|
|
71
|
+
networkBodyLimit: 500, // Truncate response body length
|
|
72
|
+
networkInclude: [], // Only these URLs
|
|
73
|
+
networkIgnore: [] // Skip these URLs
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Server Options
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
export const POST = createConsoleBridgeEndpoint({
|
|
81
|
+
prefix: '[FRONTEND', // Log prefix
|
|
82
|
+
formatter: (level, url, timestamp, args) => {
|
|
83
|
+
return `[CUSTOM] ${level} from ${url}`;
|
|
84
|
+
},
|
|
85
|
+
onLog: (level, url, timestamp, args) => {
|
|
86
|
+
// Custom handler (e.g., send to external service)
|
|
87
|
+
if (level === 'error') {
|
|
88
|
+
sendToSentry(args);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Example Output
|
|
95
|
+
|
|
96
|
+
**Browser console:**
|
|
97
|
+
```
|
|
98
|
+
Hello from frontend
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Server terminal:**
|
|
102
|
+
```
|
|
103
|
+
[FRONTEND LOG] http://localhost:5173/app/dashboard @ 2024-01-11T10:30:00.000Z Hello from frontend
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
### Client
|
|
109
|
+
|
|
110
|
+
#### `initConsolebridge(options?)`
|
|
111
|
+
|
|
112
|
+
Initialize console bridge in browser.
|
|
113
|
+
|
|
114
|
+
**Options:**
|
|
115
|
+
- `endpoint?: string` - Backend endpoint (default: `/api/console-bridge`)
|
|
116
|
+
- `batchSize?: number` - Logs per batch (default: 10)
|
|
117
|
+
- `batchDelay?: number` - Batch delay in ms (default: 100)
|
|
118
|
+
- `levels?: LogLevel[]` - Log levels to forward (default: all)
|
|
119
|
+
- `captureNetwork?: boolean` - Forward fetch/XHR calls (default: true)
|
|
120
|
+
- `captureErrors?: boolean` - Forward global errors/rejections (default: true)
|
|
121
|
+
- `networkBodyLimit?: number` - Max body chars, 0 disables (default: 500)
|
|
122
|
+
- `networkInclude?: (string | RegExp)[]` - Include-only URL patterns (default: [])
|
|
123
|
+
- `networkIgnore?: (string | RegExp)[]` - Ignore URL patterns (default: [])
|
|
124
|
+
|
|
125
|
+
#### `restoreConsole()`
|
|
126
|
+
|
|
127
|
+
Restore original console methods.
|
|
128
|
+
|
|
129
|
+
### Server
|
|
130
|
+
|
|
131
|
+
#### `createConsoleBridgeEndpoint(options?)`
|
|
132
|
+
|
|
133
|
+
Create SvelteKit RequestHandler for console bridge.
|
|
134
|
+
|
|
135
|
+
**Options:**
|
|
136
|
+
- `prefix?: string` - Log prefix (default: `[FRONTEND`)
|
|
137
|
+
- `formatter?: (level, url, timestamp, args) => string` - Custom formatter
|
|
138
|
+
- `onLog?: (level, url, timestamp, args) => void` - Custom log handler
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Build
|
|
144
|
+
pnpm build
|
|
145
|
+
|
|
146
|
+
# Publish
|
|
147
|
+
npm publish
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACN,iBAAiB,EACjB,cAAc,EACd,KAAK,oBAAoB,EACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,2BAA2B,EAC3B,KAAK,0BAA0B,EAC/B,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side console bridge for SvelteKit
|
|
3
|
+
* Intercepts console methods and forwards logs to backend
|
|
4
|
+
*/
|
|
5
|
+
export interface ConsoleBridgeOptions {
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
batchSize?: number;
|
|
8
|
+
batchDelay?: number;
|
|
9
|
+
levels?: LogLevel[];
|
|
10
|
+
captureNetwork?: boolean;
|
|
11
|
+
captureErrors?: boolean;
|
|
12
|
+
networkBodyLimit?: number;
|
|
13
|
+
networkIgnore?: (string | RegExp)[];
|
|
14
|
+
networkInclude?: (string | RegExp)[];
|
|
15
|
+
}
|
|
16
|
+
type LogLevel = 'log' | 'warn' | 'error' | 'info' | 'debug';
|
|
17
|
+
/**
|
|
18
|
+
* Initialize console bridge
|
|
19
|
+
* Call this in your root +layout.svelte onMount()
|
|
20
|
+
*/
|
|
21
|
+
export declare function initConsolebridge(userOptions?: ConsoleBridgeOptions): void;
|
|
22
|
+
/**
|
|
23
|
+
* Restore original console methods
|
|
24
|
+
*/
|
|
25
|
+
export declare function restoreConsole(): void;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CACrC;AAED,KAAK,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAmW5D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,GAAE,oBAAyB,QA4BvE;AAED;;GAEG;AACH,wBAAgB,cAAc,SAU7B"}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side console bridge for SvelteKit
|
|
3
|
+
* Intercepts console methods and forwards logs to backend
|
|
4
|
+
*/
|
|
5
|
+
import { browser, dev } from '$app/environment';
|
|
6
|
+
const DEFAULT_OPTIONS = {
|
|
7
|
+
endpoint: '/api/console-bridge',
|
|
8
|
+
batchSize: 10,
|
|
9
|
+
batchDelay: 100,
|
|
10
|
+
levels: ['log', 'warn', 'error', 'info', 'debug'],
|
|
11
|
+
captureNetwork: true,
|
|
12
|
+
captureErrors: true,
|
|
13
|
+
networkBodyLimit: 500,
|
|
14
|
+
networkIgnore: [],
|
|
15
|
+
networkInclude: []
|
|
16
|
+
};
|
|
17
|
+
let isSending = false;
|
|
18
|
+
let logQueue = [];
|
|
19
|
+
let batchTimer = null;
|
|
20
|
+
let options = null;
|
|
21
|
+
let isInitialized = false;
|
|
22
|
+
let networkPatched = false;
|
|
23
|
+
let errorListenersAttached = false;
|
|
24
|
+
let originalFetch = null;
|
|
25
|
+
let originalXhrOpen = null;
|
|
26
|
+
let originalXhrSend = null;
|
|
27
|
+
let errorHandler = null;
|
|
28
|
+
let rejectionHandler = null;
|
|
29
|
+
const xhrMeta = new WeakMap();
|
|
30
|
+
// Store original console methods
|
|
31
|
+
const originalConsole = {
|
|
32
|
+
log: console.log,
|
|
33
|
+
warn: console.warn,
|
|
34
|
+
error: console.error,
|
|
35
|
+
info: console.info,
|
|
36
|
+
debug: console.debug
|
|
37
|
+
};
|
|
38
|
+
function resolveUrl(input) {
|
|
39
|
+
try {
|
|
40
|
+
return new URL(input, window.location.href).toString();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return input;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function matchesPattern(value, pattern) {
|
|
47
|
+
if (pattern instanceof RegExp)
|
|
48
|
+
return pattern.test(value);
|
|
49
|
+
return value.includes(pattern);
|
|
50
|
+
}
|
|
51
|
+
function isNetworkTracked(targetUrl) {
|
|
52
|
+
if (!options)
|
|
53
|
+
return false;
|
|
54
|
+
const resolved = resolveUrl(targetUrl);
|
|
55
|
+
if (resolved === resolveUrl(options.endpoint))
|
|
56
|
+
return false;
|
|
57
|
+
if (options.networkIgnore.some((pattern) => matchesPattern(resolved, pattern))) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (options.networkInclude.length > 0) {
|
|
61
|
+
return options.networkInclude.some((pattern) => matchesPattern(resolved, pattern));
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
function queueEntry(entry) {
|
|
66
|
+
if (!dev || !browser || !options)
|
|
67
|
+
return;
|
|
68
|
+
if (isSending)
|
|
69
|
+
return;
|
|
70
|
+
logQueue.push(entry);
|
|
71
|
+
scheduleBatch();
|
|
72
|
+
}
|
|
73
|
+
function sendBatch() {
|
|
74
|
+
if (!options || isSending || logQueue.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
isSending = true;
|
|
77
|
+
const batch = logQueue.splice(0, options.batchSize);
|
|
78
|
+
const sender = originalFetch ?? fetch;
|
|
79
|
+
sender(options.endpoint, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify(batch.length === 1 ? batch[0] : { batch })
|
|
83
|
+
})
|
|
84
|
+
.catch((err) => {
|
|
85
|
+
originalConsole.error('[Console Bridge] Failed to send logs:', err);
|
|
86
|
+
})
|
|
87
|
+
.finally(() => {
|
|
88
|
+
isSending = false;
|
|
89
|
+
if (logQueue.length > 0) {
|
|
90
|
+
scheduleBatch();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function scheduleBatch() {
|
|
95
|
+
if (!options || batchTimer)
|
|
96
|
+
return;
|
|
97
|
+
batchTimer = setTimeout(() => {
|
|
98
|
+
batchTimer = null;
|
|
99
|
+
sendBatch();
|
|
100
|
+
}, options.batchDelay);
|
|
101
|
+
}
|
|
102
|
+
function createInterceptor(level) {
|
|
103
|
+
return function (...args) {
|
|
104
|
+
originalConsole[level](...args);
|
|
105
|
+
if (dev && browser && options?.levels.includes(level)) {
|
|
106
|
+
const entry = {
|
|
107
|
+
kind: 'console',
|
|
108
|
+
level,
|
|
109
|
+
args,
|
|
110
|
+
timestamp: new Date().toISOString(),
|
|
111
|
+
url: window.location.href
|
|
112
|
+
};
|
|
113
|
+
// Capture stack trace for errors
|
|
114
|
+
if (level === 'error' && args[0] instanceof Error) {
|
|
115
|
+
const error = args[0];
|
|
116
|
+
if (error.stack) {
|
|
117
|
+
// Truncate to prevent huge payloads
|
|
118
|
+
entry.stack = error.stack.slice(0, 1000);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
queueEntry(entry);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function getFetchDetails(args) {
|
|
126
|
+
const [input, init] = args;
|
|
127
|
+
if (input instanceof Request) {
|
|
128
|
+
return { method: input.method ?? 'GET', url: input.url };
|
|
129
|
+
}
|
|
130
|
+
const url = typeof input === 'string' || input instanceof URL ? input.toString() : String(input);
|
|
131
|
+
return { method: init?.method ?? 'GET', url };
|
|
132
|
+
}
|
|
133
|
+
async function readResponseBody(response) {
|
|
134
|
+
if (!options || options.networkBodyLimit <= 0)
|
|
135
|
+
return undefined;
|
|
136
|
+
try {
|
|
137
|
+
const clone = response.clone();
|
|
138
|
+
const text = await clone.text();
|
|
139
|
+
return text.slice(0, options.networkBodyLimit);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function readXhrBody(xhr) {
|
|
146
|
+
if (!options || options.networkBodyLimit <= 0)
|
|
147
|
+
return undefined;
|
|
148
|
+
try {
|
|
149
|
+
if (xhr.responseType && xhr.responseType !== 'text')
|
|
150
|
+
return undefined;
|
|
151
|
+
const text = typeof xhr.responseText === 'string' ? xhr.responseText : '';
|
|
152
|
+
return text.slice(0, options.networkBodyLimit);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function patchFetch() {
|
|
159
|
+
if (!('fetch' in window))
|
|
160
|
+
return;
|
|
161
|
+
if (!originalFetch)
|
|
162
|
+
originalFetch = window.fetch.bind(window);
|
|
163
|
+
window.fetch = (async (...args) => {
|
|
164
|
+
const [input, init] = args;
|
|
165
|
+
const { method, url } = getFetchDetails(args);
|
|
166
|
+
const resolvedUrl = resolveUrl(url);
|
|
167
|
+
if (!isNetworkTracked(resolvedUrl)) {
|
|
168
|
+
return originalFetch.call(window, input, init);
|
|
169
|
+
}
|
|
170
|
+
const start = performance.now();
|
|
171
|
+
try {
|
|
172
|
+
const response = await originalFetch.call(window, input, init);
|
|
173
|
+
const duration = Math.round(performance.now() - start);
|
|
174
|
+
const responseBody = await readResponseBody(response);
|
|
175
|
+
queueEntry({
|
|
176
|
+
kind: 'network',
|
|
177
|
+
level: 'network',
|
|
178
|
+
args: [`${method} ${resolvedUrl}`, `status: ${response.status}`, `duration: ${duration}ms`],
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
url: resolvedUrl,
|
|
181
|
+
method,
|
|
182
|
+
status: response.status,
|
|
183
|
+
duration,
|
|
184
|
+
requestType: 'fetch',
|
|
185
|
+
pageUrl: window.location.href,
|
|
186
|
+
responseBody
|
|
187
|
+
});
|
|
188
|
+
return response;
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const duration = Math.round(performance.now() - start);
|
|
192
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
193
|
+
const stack = err instanceof Error && err.stack ? err.stack.slice(0, 1000) : undefined;
|
|
194
|
+
queueEntry({
|
|
195
|
+
kind: 'network',
|
|
196
|
+
level: 'network',
|
|
197
|
+
args: [`${method} ${resolvedUrl}`, `error: ${message}`, `duration: ${duration}ms`],
|
|
198
|
+
timestamp: new Date().toISOString(),
|
|
199
|
+
url: resolvedUrl,
|
|
200
|
+
method,
|
|
201
|
+
status: 0,
|
|
202
|
+
duration,
|
|
203
|
+
requestType: 'fetch',
|
|
204
|
+
pageUrl: window.location.href,
|
|
205
|
+
stack
|
|
206
|
+
});
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function patchXhr() {
|
|
212
|
+
if (!('XMLHttpRequest' in window))
|
|
213
|
+
return;
|
|
214
|
+
if (!originalXhrOpen)
|
|
215
|
+
originalXhrOpen = XMLHttpRequest.prototype.open;
|
|
216
|
+
if (!originalXhrSend)
|
|
217
|
+
originalXhrSend = XMLHttpRequest.prototype.send;
|
|
218
|
+
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
|
219
|
+
xhrMeta.set(this, { method, url, start: 0 });
|
|
220
|
+
return originalXhrOpen.apply(this, [method, url, ...rest]);
|
|
221
|
+
};
|
|
222
|
+
XMLHttpRequest.prototype.send = function (...args) {
|
|
223
|
+
const meta = xhrMeta.get(this);
|
|
224
|
+
if (meta)
|
|
225
|
+
meta.start = performance.now();
|
|
226
|
+
const resolvedUrl = meta ? resolveUrl(meta.url) : '';
|
|
227
|
+
const onDone = async () => {
|
|
228
|
+
this.removeEventListener('loadend', onDone);
|
|
229
|
+
if (!meta || !resolvedUrl || !isNetworkTracked(resolvedUrl))
|
|
230
|
+
return;
|
|
231
|
+
const duration = Math.round(performance.now() - meta.start);
|
|
232
|
+
const status = this.status || 0;
|
|
233
|
+
const responseBody = await readXhrBody(this);
|
|
234
|
+
queueEntry({
|
|
235
|
+
kind: 'network',
|
|
236
|
+
level: 'network',
|
|
237
|
+
args: [`${meta.method} ${resolvedUrl}`, `status: ${status}`, `duration: ${duration}ms`],
|
|
238
|
+
timestamp: new Date().toISOString(),
|
|
239
|
+
url: resolvedUrl,
|
|
240
|
+
method: meta.method,
|
|
241
|
+
status,
|
|
242
|
+
duration,
|
|
243
|
+
requestType: 'xhr',
|
|
244
|
+
pageUrl: window.location.href,
|
|
245
|
+
responseBody
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
this.addEventListener('loadend', onDone);
|
|
249
|
+
const body = args[0];
|
|
250
|
+
return originalXhrSend.call(this, body);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function restoreNetwork() {
|
|
254
|
+
if (!networkPatched)
|
|
255
|
+
return;
|
|
256
|
+
if (originalFetch)
|
|
257
|
+
window.fetch = originalFetch;
|
|
258
|
+
if (originalXhrOpen)
|
|
259
|
+
XMLHttpRequest.prototype.open = originalXhrOpen;
|
|
260
|
+
if (originalXhrSend)
|
|
261
|
+
XMLHttpRequest.prototype.send = originalXhrSend;
|
|
262
|
+
networkPatched = false;
|
|
263
|
+
}
|
|
264
|
+
function patchNetwork() {
|
|
265
|
+
if (networkPatched)
|
|
266
|
+
return;
|
|
267
|
+
networkPatched = true;
|
|
268
|
+
patchFetch();
|
|
269
|
+
patchXhr();
|
|
270
|
+
}
|
|
271
|
+
function attachErrorListeners() {
|
|
272
|
+
if (errorListenersAttached)
|
|
273
|
+
return;
|
|
274
|
+
errorListenersAttached = true;
|
|
275
|
+
errorHandler = (event) => {
|
|
276
|
+
const stack = event.error?.stack ? event.error.stack.slice(0, 1000) : undefined;
|
|
277
|
+
queueEntry({
|
|
278
|
+
kind: 'error',
|
|
279
|
+
level: 'error',
|
|
280
|
+
args: [event.message],
|
|
281
|
+
timestamp: new Date().toISOString(),
|
|
282
|
+
url: event.filename ?? window.location.href,
|
|
283
|
+
stack,
|
|
284
|
+
pageUrl: window.location.href
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
rejectionHandler = (event) => {
|
|
288
|
+
const reason = event.reason;
|
|
289
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
290
|
+
const stack = reason instanceof Error && reason.stack ? reason.stack.slice(0, 1000) : undefined;
|
|
291
|
+
queueEntry({
|
|
292
|
+
kind: 'error',
|
|
293
|
+
level: 'error',
|
|
294
|
+
args: [message],
|
|
295
|
+
timestamp: new Date().toISOString(),
|
|
296
|
+
url: window.location.href,
|
|
297
|
+
stack,
|
|
298
|
+
pageUrl: window.location.href
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
window.addEventListener('error', errorHandler);
|
|
302
|
+
window.addEventListener('unhandledrejection', rejectionHandler);
|
|
303
|
+
}
|
|
304
|
+
function detachErrorListeners() {
|
|
305
|
+
if (!errorListenersAttached)
|
|
306
|
+
return;
|
|
307
|
+
errorListenersAttached = false;
|
|
308
|
+
if (errorHandler)
|
|
309
|
+
window.removeEventListener('error', errorHandler);
|
|
310
|
+
if (rejectionHandler)
|
|
311
|
+
window.removeEventListener('unhandledrejection', rejectionHandler);
|
|
312
|
+
errorHandler = null;
|
|
313
|
+
rejectionHandler = null;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Initialize console bridge
|
|
317
|
+
* Call this in your root +layout.svelte onMount()
|
|
318
|
+
*/
|
|
319
|
+
export function initConsolebridge(userOptions = {}) {
|
|
320
|
+
if (!browser || !dev)
|
|
321
|
+
return;
|
|
322
|
+
options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
323
|
+
// Intercept only specified levels
|
|
324
|
+
if (options.levels.includes('log'))
|
|
325
|
+
console.log = createInterceptor('log');
|
|
326
|
+
if (options.levels.includes('warn'))
|
|
327
|
+
console.warn = createInterceptor('warn');
|
|
328
|
+
if (options.levels.includes('error'))
|
|
329
|
+
console.error = createInterceptor('error');
|
|
330
|
+
if (options.levels.includes('info'))
|
|
331
|
+
console.info = createInterceptor('info');
|
|
332
|
+
if (options.levels.includes('debug'))
|
|
333
|
+
console.debug = createInterceptor('debug');
|
|
334
|
+
if (options.captureNetwork) {
|
|
335
|
+
patchNetwork();
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
restoreNetwork();
|
|
339
|
+
}
|
|
340
|
+
if (options.captureErrors) {
|
|
341
|
+
attachErrorListeners();
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
detachErrorListeners();
|
|
345
|
+
}
|
|
346
|
+
if (!isInitialized) {
|
|
347
|
+
isInitialized = true;
|
|
348
|
+
originalConsole.info(`[Console Bridge] Active - logs forwarded to ${options.endpoint}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Restore original console methods
|
|
353
|
+
*/
|
|
354
|
+
export function restoreConsole() {
|
|
355
|
+
console.log = originalConsole.log;
|
|
356
|
+
console.warn = originalConsole.warn;
|
|
357
|
+
console.error = originalConsole.error;
|
|
358
|
+
console.info = originalConsole.info;
|
|
359
|
+
console.debug = originalConsole.debug;
|
|
360
|
+
restoreNetwork();
|
|
361
|
+
detachErrorListeners();
|
|
362
|
+
options = null;
|
|
363
|
+
isInitialized = false;
|
|
364
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side console bridge endpoint factory
|
|
3
|
+
*/
|
|
4
|
+
import type { RequestHandler } from '@sveltejs/kit';
|
|
5
|
+
export interface ConsoleBridgeServerOptions {
|
|
6
|
+
prefix?: string;
|
|
7
|
+
formatter?: (level: string, url: string, timestamp: string, args: any[]) => string;
|
|
8
|
+
onLog?: (level: string, url: string, timestamp: string, args: any[]) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Create a SvelteKit RequestHandler for console bridge endpoint
|
|
12
|
+
* Use in your +server.ts: export const POST = createConsoleBridgeEndpoint();
|
|
13
|
+
*/
|
|
14
|
+
export declare function createConsoleBridgeEndpoint(userOptions?: ConsoleBridgeServerOptions): RequestHandler;
|
|
15
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/lib/server.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,MAAM,WAAW,0BAA0B;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,MAAM,CAAC;IACnF,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;CAC7E;AASD;;;GAGG;AACH,wBAAgB,2BAA2B,CAC1C,WAAW,GAAE,0BAA+B,GAC1C,cAAc,CAuDhB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side console bridge endpoint factory
|
|
3
|
+
*/
|
|
4
|
+
import { json, error } from '@sveltejs/kit';
|
|
5
|
+
const DEFAULT_OPTIONS = {
|
|
6
|
+
prefix: '[FRONTEND',
|
|
7
|
+
formatter: (level, url, timestamp) => `[FRONTEND ${level.toUpperCase()}] ${url} @ ${timestamp}`,
|
|
8
|
+
onLog: () => { }
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Create a SvelteKit RequestHandler for console bridge endpoint
|
|
12
|
+
* Use in your +server.ts: export const POST = createConsoleBridgeEndpoint();
|
|
13
|
+
*/
|
|
14
|
+
export function createConsoleBridgeEndpoint(userOptions = {}) {
|
|
15
|
+
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
16
|
+
return async ({ request }) => {
|
|
17
|
+
// Only allow in dev mode
|
|
18
|
+
if (import.meta.env.PROD) {
|
|
19
|
+
throw error(404);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const body = await request.json();
|
|
23
|
+
// Handle both single log and batch
|
|
24
|
+
const logs = Array.isArray(body.batch) ? body.batch : [body];
|
|
25
|
+
for (const log of logs) {
|
|
26
|
+
const { level, args, timestamp, url, stack } = log;
|
|
27
|
+
// Custom formatter or default
|
|
28
|
+
const prefix = options.formatter(level, url, timestamp, args);
|
|
29
|
+
// Prepare args for logging (include stack if present)
|
|
30
|
+
const logArgs = stack ? [...args, `\nStack: ${stack}`] : args;
|
|
31
|
+
// Log to console
|
|
32
|
+
switch (level) {
|
|
33
|
+
case 'error':
|
|
34
|
+
console.error(prefix, ...logArgs);
|
|
35
|
+
break;
|
|
36
|
+
case 'warn':
|
|
37
|
+
console.warn(prefix, ...logArgs);
|
|
38
|
+
break;
|
|
39
|
+
case 'info':
|
|
40
|
+
console.info(prefix, ...logArgs);
|
|
41
|
+
break;
|
|
42
|
+
case 'debug':
|
|
43
|
+
console.debug(prefix, ...logArgs);
|
|
44
|
+
break;
|
|
45
|
+
case 'network':
|
|
46
|
+
console.info(prefix, ...logArgs);
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
console.log(prefix, ...logArgs);
|
|
50
|
+
}
|
|
51
|
+
// Custom callback
|
|
52
|
+
options.onLog(level, url, timestamp, args);
|
|
53
|
+
}
|
|
54
|
+
return json({ success: true });
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.error('[Console Bridge] Failed to process logs:', err);
|
|
58
|
+
return json({ success: false }, { status: 400 });
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "console-bridge-sveltekit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Forward frontend console logs to backend for easier debugging in SvelteKit",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./client": {
|
|
12
|
+
"types": "./dist/lib/client.d.ts",
|
|
13
|
+
"import": "./dist/lib/client.js"
|
|
14
|
+
},
|
|
15
|
+
"./server": {
|
|
16
|
+
"types": "./dist/lib/server.d.ts",
|
|
17
|
+
"import": "./dist/lib/server.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"prepublishOnly": "pnpm build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"sveltekit",
|
|
31
|
+
"console",
|
|
32
|
+
"logging",
|
|
33
|
+
"debugging",
|
|
34
|
+
"development"
|
|
35
|
+
],
|
|
36
|
+
"author": "Chintan Thakkar",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@sveltejs/kit": "^2.0.0",
|
|
40
|
+
"svelte": "^5.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@sveltejs/kit": "^2.0.0",
|
|
44
|
+
"svelte": "^5.0.0",
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|