@watchforge/browser 0.1.4 → 0.1.5
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/CONFIGURATION_GUIDE.md +138 -10
- package/README.md +138 -1
- package/bin/watchforge.js +38 -12
- package/package.json +31 -6
- package/src/express.d.ts +12 -0
- package/src/index.d.ts +41 -0
- package/src/index.js +1 -2
- package/src/next-server.d.ts +26 -0
- package/src/next-server.js +58 -0
- package/src/next.d.ts +11 -0
- package/src/next.js +12 -0
- package/src/react.d.ts +8 -0
- package/src/stacktrace.js +179 -3
- package/src/tracing.d.ts +39 -0
package/CONFIGURATION_GUIDE.md
CHANGED
|
@@ -2,10 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
The WatchForge JavaScript SDK
|
|
5
|
+
The WatchForge JavaScript SDK is one npm package with typed framework entry points for:
|
|
6
|
+
|
|
7
|
+
- **Browser JavaScript** applications
|
|
8
|
+
- **Next.js** App Router applications
|
|
9
|
+
- **React** frontend applications
|
|
6
10
|
- **Node.js** applications
|
|
7
11
|
- **Express.js** web servers
|
|
8
|
-
|
|
12
|
+
|
|
13
|
+
This package intentionally stays unified instead of splitting into separate `watchforge-next-sdk`, `watchforge-node-sdk`, `watchforge-react-sdk`, and `watchforge-express-sdk` packages. The shared package keeps DSN handling, event transport, breadcrumbs, stack traces, source context, and session replay consistent across JavaScript runtimes.
|
|
14
|
+
|
|
15
|
+
## Supported Imports
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// Browser JavaScript, React client, and shared APIs
|
|
19
|
+
import { register, captureException, captureMessage } from "@watchforge/browser";
|
|
20
|
+
|
|
21
|
+
// Next.js App Router client provider
|
|
22
|
+
import { WatchForgeProvider } from "@watchforge/browser/next";
|
|
23
|
+
|
|
24
|
+
// Next.js server route handlers
|
|
25
|
+
import { withWatchForgeRouteHandler } from "@watchforge/browser/next/server";
|
|
26
|
+
|
|
27
|
+
// React error boundary
|
|
28
|
+
import { ErrorBoundary } from "@watchforge/browser/react";
|
|
29
|
+
|
|
30
|
+
// Express middleware
|
|
31
|
+
import {
|
|
32
|
+
expressRequestMiddleware,
|
|
33
|
+
expressMiddleware,
|
|
34
|
+
} from "@watchforge/browser/express";
|
|
35
|
+
|
|
36
|
+
// Explicit Node.js import
|
|
37
|
+
import { register as registerNode } from "@watchforge/browser/node";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Support Matrix
|
|
41
|
+
|
|
42
|
+
| Runtime / Framework | Entry point | Recommended setup |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| Browser JavaScript | `@watchforge/browser` | Call `register()` once in the main browser bundle |
|
|
45
|
+
| React | `@watchforge/browser` + `@watchforge/browser/react` | Call `register()` once and wrap the app with `ErrorBoundary` |
|
|
46
|
+
| Next.js App Router client | `@watchforge/browser/next` | Use the wizard or render `WatchForgeProvider` from a client component |
|
|
47
|
+
| Next.js App Router server | `@watchforge/browser/next/server` | Wrap route handlers with `withWatchForgeRouteHandler()` |
|
|
48
|
+
| Node.js | `@watchforge/browser/node` | Call `register()` once during process startup |
|
|
49
|
+
| Express.js | `@watchforge/browser/express` | Add request middleware before routes and error middleware after routes |
|
|
9
50
|
|
|
10
51
|
## Installation
|
|
11
52
|
|
|
@@ -64,8 +105,9 @@ npm install ../watchforge-javascript-sdk
|
|
|
64
105
|
```
|
|
65
106
|
|
|
66
107
|
After installation, use it the same way:
|
|
67
|
-
```
|
|
68
|
-
import { register
|
|
108
|
+
```ts
|
|
109
|
+
import { register } from "@watchforge/browser";
|
|
110
|
+
import { ErrorBoundary } from "@watchforge/browser/react";
|
|
69
111
|
```
|
|
70
112
|
|
|
71
113
|
### Next.js Wizard Setup
|
|
@@ -89,7 +131,7 @@ The wizard:
|
|
|
89
131
|
|
|
90
132
|
1. Installs `@watchforge/browser`
|
|
91
133
|
2. Creates `watchforge.config.ts`
|
|
92
|
-
3. Creates `app/watchforge-init.tsx` (or `src/app
|
|
134
|
+
3. Creates `app/watchforge-init.tsx` (or `src/app/.../watchforge-init.tsx`)
|
|
93
135
|
4. Patches `app/layout.tsx` to render `<WatchForgeInit />`
|
|
94
136
|
|
|
95
137
|
If you only want file generation and no package install:
|
|
@@ -188,11 +230,92 @@ See the React setup section below for complete examples.
|
|
|
188
230
|
|
|
189
231
|
## Quick Start
|
|
190
232
|
|
|
233
|
+
### Browser JavaScript
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { register } from "@watchforge/browser";
|
|
237
|
+
|
|
238
|
+
register({
|
|
239
|
+
dsn: "https://PUBLIC_KEY@watchforge.io/PROJECT_ID",
|
|
240
|
+
app_env: "production",
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
This captures uncaught browser errors, unhandled promise rejections, breadcrumbs, page context, browser/device/OS details, and performance context.
|
|
245
|
+
|
|
246
|
+
### Next.js App Router
|
|
247
|
+
|
|
248
|
+
Create `watchforge.config.ts` in your project root:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
export const watchforgeConfig = {
|
|
252
|
+
dsn: "https://PUBLIC_KEY@watchforge.io/PROJECT_ID",
|
|
253
|
+
app_env: "production",
|
|
254
|
+
replaysOnErrorSampleRate: 1,
|
|
255
|
+
maskAllInputs: true,
|
|
256
|
+
};
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Create a client init component beside your frontend layout, for example `src/app/watchforge-init.tsx` or `src/app/(frontend)/watchforge-init.tsx`:
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
"use client";
|
|
263
|
+
|
|
264
|
+
import { WatchForgeProvider } from "@watchforge/browser/next";
|
|
265
|
+
import { watchforgeConfig } from "../watchforge.config";
|
|
266
|
+
|
|
267
|
+
export default function WatchForgeInit() {
|
|
268
|
+
return <WatchForgeProvider options={watchforgeConfig} />;
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Render it once in the matching `layout.tsx`:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import WatchForgeInit from "./watchforge-init";
|
|
276
|
+
|
|
277
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
278
|
+
return (
|
|
279
|
+
<html lang="en">
|
|
280
|
+
<body>
|
|
281
|
+
<WatchForgeInit />
|
|
282
|
+
{children}
|
|
283
|
+
</body>
|
|
284
|
+
</html>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
For route-group projects like `src/app/(frontend)/layout.tsx`, put `watchforge-init.tsx` in that same route group and adjust the config import path if needed.
|
|
290
|
+
|
|
291
|
+
For Next.js route handlers, use the server entry point:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
// app/api/example/route.ts
|
|
295
|
+
import {
|
|
296
|
+
register,
|
|
297
|
+
withWatchForgeRouteHandler,
|
|
298
|
+
} from "@watchforge/browser/next/server";
|
|
299
|
+
|
|
300
|
+
register({
|
|
301
|
+
dsn: "https://PUBLIC_KEY@watchforge.io/PROJECT_ID",
|
|
302
|
+
app_env: "production",
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
export const GET = withWatchForgeRouteHandler(async () => {
|
|
306
|
+
throw new Error("Next.js API route test error");
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
191
310
|
### 1. Node.js Application
|
|
192
311
|
|
|
193
|
-
```
|
|
312
|
+
```ts
|
|
194
313
|
// app.js
|
|
195
|
-
|
|
314
|
+
import {
|
|
315
|
+
register,
|
|
316
|
+
captureException,
|
|
317
|
+
captureMessage,
|
|
318
|
+
} from "@watchforge/browser/node";
|
|
196
319
|
|
|
197
320
|
// Initialize SDK
|
|
198
321
|
// API URL is automatically derived from DSN host
|
|
@@ -214,10 +337,14 @@ captureMessage("User logged in", "info");
|
|
|
214
337
|
|
|
215
338
|
### 2. Express.js Application
|
|
216
339
|
|
|
217
|
-
```
|
|
340
|
+
```ts
|
|
218
341
|
// server.js
|
|
219
|
-
|
|
220
|
-
|
|
342
|
+
import express from "express";
|
|
343
|
+
import { register } from "@watchforge/browser/node";
|
|
344
|
+
import {
|
|
345
|
+
expressRequestMiddleware,
|
|
346
|
+
expressMiddleware,
|
|
347
|
+
} from "@watchforge/browser/express";
|
|
221
348
|
|
|
222
349
|
const app = express();
|
|
223
350
|
|
|
@@ -755,6 +882,7 @@ register({
|
|
|
755
882
|
|
|
756
883
|
app.use(express.json());
|
|
757
884
|
app.use(expressRequestMiddleware());
|
|
885
|
+
app.use(expressRequestMiddleware());
|
|
758
886
|
|
|
759
887
|
// routes here
|
|
760
888
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# WatchForge JavaScript SDK (`@watchforge/browser`)
|
|
2
2
|
|
|
3
|
-
Browser
|
|
3
|
+
Browser, Next.js, React, Node.js, and Express SDK for WatchForge. **One call to `register()`** turns on automatic error reporting for typical crashes, and framework entry points add richer context where needed.
|
|
4
|
+
|
|
5
|
+
## Runtime Support
|
|
6
|
+
|
|
7
|
+
| Runtime / Framework | Import | What it captures |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Browser JavaScript | `@watchforge/browser` | Uncaught errors, unhandled promise rejections, breadcrumbs, browser/device/page/performance context |
|
|
10
|
+
| React | `@watchforge/browser` + `@watchforge/browser/react` | Browser errors plus React `ErrorBoundary` component stack context |
|
|
11
|
+
| Next.js App Router | `@watchforge/browser/next` | Client-side Next.js errors through a client provider; wizard patches the app layout |
|
|
12
|
+
| Node.js | `@watchforge/browser` or `@watchforge/browser/node` | `uncaughtException`, `unhandledRejection`, Node runtime/server context |
|
|
13
|
+
| Express.js | `@watchforge/browser/express` | Express request errors, request URL/method/headers/body/query, user/IP, route, duration |
|
|
14
|
+
|
|
15
|
+
The package is intentionally shipped as **one npm package** with typed subpath exports instead of separate SDK packages. This keeps DSN setup, replay, stack traces, breadcrumbs, and event transport consistent across JavaScript runtimes.
|
|
4
16
|
|
|
5
17
|
## What this package does *not* require
|
|
6
18
|
|
|
@@ -37,6 +49,131 @@ npx @watchforge/browser -i nextjs \
|
|
|
37
49
|
|
|
38
50
|
The wizard installs `@watchforge/browser`, writes `watchforge.config.ts`, creates a client init component, and patches `app/layout.tsx` or `pages/_app.tsx`.
|
|
39
51
|
|
|
52
|
+
## Framework Imports
|
|
53
|
+
|
|
54
|
+
WatchForge ships as one JavaScript SDK package with typed framework entry points:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// Browser / React client / Node global handlers
|
|
58
|
+
import { register } from "@watchforge/browser";
|
|
59
|
+
|
|
60
|
+
// Next.js App Router client provider
|
|
61
|
+
import { WatchForgeProvider } from "@watchforge/browser/next";
|
|
62
|
+
|
|
63
|
+
// Next.js App Router server route helper
|
|
64
|
+
import { withWatchForgeRouteHandler } from "@watchforge/browser/next/server";
|
|
65
|
+
|
|
66
|
+
// React error boundary
|
|
67
|
+
import { ErrorBoundary } from "@watchforge/browser/react";
|
|
68
|
+
|
|
69
|
+
// Express middleware
|
|
70
|
+
import {
|
|
71
|
+
expressRequestMiddleware,
|
|
72
|
+
expressMiddleware,
|
|
73
|
+
} from "@watchforge/browser/express";
|
|
74
|
+
|
|
75
|
+
// Optional explicit Node import
|
|
76
|
+
import { captureException } from "@watchforge/browser/node";
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Next.js Manual Setup
|
|
80
|
+
|
|
81
|
+
Use the wizard when possible. For manual setup in App Router projects, create a client init component for browser-side errors:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
"use client";
|
|
85
|
+
|
|
86
|
+
import { WatchForgeProvider } from "@watchforge/browser/next";
|
|
87
|
+
import { watchforgeConfig } from "../watchforge.config";
|
|
88
|
+
|
|
89
|
+
export default function WatchForgeInit() {
|
|
90
|
+
return <WatchForgeProvider options={watchforgeConfig} />;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Then render it once in your frontend `layout.tsx`:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import WatchForgeInit from "./watchforge-init";
|
|
98
|
+
|
|
99
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
100
|
+
return (
|
|
101
|
+
<html lang="en">
|
|
102
|
+
<body>
|
|
103
|
+
<WatchForgeInit />
|
|
104
|
+
{children}
|
|
105
|
+
</body>
|
|
106
|
+
</html>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For route groups, place the init component beside the frontend layout, for example `src/app/(frontend)/watchforge-init.tsx`, and import `watchforge.config.ts` using the correct relative path.
|
|
112
|
+
|
|
113
|
+
For server route handlers, initialize once in server code and wrap handlers:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import {
|
|
117
|
+
register,
|
|
118
|
+
withWatchForgeRouteHandler,
|
|
119
|
+
} from "@watchforge/browser/next/server";
|
|
120
|
+
|
|
121
|
+
register({
|
|
122
|
+
dsn: "https://PUBLIC_KEY@your-host/PROJECT_ID",
|
|
123
|
+
app_env: "production",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export const GET = withWatchForgeRouteHandler(async () => {
|
|
127
|
+
throw new Error("Next.js route handler test error");
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Express Setup
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import express from "express";
|
|
135
|
+
import { register } from "@watchforge/browser/node";
|
|
136
|
+
import {
|
|
137
|
+
expressRequestMiddleware,
|
|
138
|
+
expressMiddleware,
|
|
139
|
+
} from "@watchforge/browser/express";
|
|
140
|
+
|
|
141
|
+
register({
|
|
142
|
+
dsn: "https://PUBLIC_KEY@your-host/PROJECT_ID",
|
|
143
|
+
app_env: "production",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const app = express();
|
|
147
|
+
app.use(express.json());
|
|
148
|
+
app.use(expressRequestMiddleware());
|
|
149
|
+
|
|
150
|
+
app.get("/error", () => {
|
|
151
|
+
throw new Error("Express test error");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
app.use(expressMiddleware());
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## React Setup
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { register } from "@watchforge/browser";
|
|
161
|
+
import { ErrorBoundary } from "@watchforge/browser/react";
|
|
162
|
+
|
|
163
|
+
register({
|
|
164
|
+
dsn: "https://PUBLIC_KEY@your-host/PROJECT_ID",
|
|
165
|
+
app_env: "production",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export function AppRoot() {
|
|
169
|
+
return (
|
|
170
|
+
<ErrorBoundary fallback={<div>Something went wrong.</div>}>
|
|
171
|
+
<App />
|
|
172
|
+
</ErrorBoundary>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
40
177
|
## Quick start (all most apps need)
|
|
41
178
|
|
|
42
179
|
Call **`register()` once** as early as possible (e.g. app entry / main bundle), with your **DSN** from the WatchForge project settings.
|
package/bin/watchforge.js
CHANGED
|
@@ -84,6 +84,15 @@ function fileExists(filePath) {
|
|
|
84
84
|
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function commandExists(command) {
|
|
88
|
+
try {
|
|
89
|
+
execSync(`command -v ${command}`, { stdio: "ignore" });
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
function findNextLayout(cwd) {
|
|
88
97
|
const candidates = [
|
|
89
98
|
path.join(cwd, "src", "app", "layout.tsx"),
|
|
@@ -91,6 +100,24 @@ function findNextLayout(cwd) {
|
|
|
91
100
|
path.join(cwd, "app", "layout.tsx"),
|
|
92
101
|
path.join(cwd, "app", "layout.jsx"),
|
|
93
102
|
];
|
|
103
|
+
|
|
104
|
+
for (const appRoot of [path.join(cwd, "src", "app"), path.join(cwd, "app")]) {
|
|
105
|
+
if (!fs.existsSync(appRoot) || !fs.statSync(appRoot).isDirectory()) continue;
|
|
106
|
+
const entries = fs.readdirSync(appRoot, { withFileTypes: true }).sort((a, b) => {
|
|
107
|
+
const score = (name) => {
|
|
108
|
+
if (name.includes("frontend") || name.includes("site")) return -1;
|
|
109
|
+
if (name.includes("payload") || name.includes("admin")) return 1;
|
|
110
|
+
return 0;
|
|
111
|
+
};
|
|
112
|
+
return score(a.name) - score(b.name) || a.name.localeCompare(b.name);
|
|
113
|
+
});
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
if (!entry.isDirectory()) continue;
|
|
116
|
+
candidates.push(path.join(appRoot, entry.name, "layout.tsx"));
|
|
117
|
+
candidates.push(path.join(appRoot, entry.name, "layout.jsx"));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
94
121
|
return candidates.find(fileExists) || null;
|
|
95
122
|
}
|
|
96
123
|
|
|
@@ -138,11 +165,11 @@ function installPackage(cwd, skipInstall) {
|
|
|
138
165
|
const hasYarn = fileExists(path.join(cwd, "yarn.lock"));
|
|
139
166
|
const hasBun = fileExists(path.join(cwd, "bun.lockb"));
|
|
140
167
|
|
|
141
|
-
const command = hasPnpm
|
|
168
|
+
const command = hasPnpm && commandExists("pnpm")
|
|
142
169
|
? "pnpm add @watchforge/browser"
|
|
143
|
-
: hasYarn
|
|
170
|
+
: hasYarn && commandExists("yarn")
|
|
144
171
|
? "yarn add @watchforge/browser"
|
|
145
|
-
: hasBun
|
|
172
|
+
: hasBun && commandExists("bun")
|
|
146
173
|
? "bun add @watchforge/browser"
|
|
147
174
|
: "npm install @watchforge/browser";
|
|
148
175
|
|
|
@@ -152,22 +179,21 @@ function installPackage(cwd, skipInstall) {
|
|
|
152
179
|
|
|
153
180
|
function patchAppRouter(cwd, layoutPath) {
|
|
154
181
|
const appDir = path.dirname(layoutPath);
|
|
155
|
-
const
|
|
156
|
-
|
|
182
|
+
const configPath = path.join(cwd, "watchforge.config.ts");
|
|
183
|
+
let configImport = path.relative(appDir, configPath).replace(/\\/g, "/");
|
|
184
|
+
configImport = configImport.replace(/\.ts$/, "");
|
|
185
|
+
if (!configImport.startsWith(".")) {
|
|
186
|
+
configImport = `./${configImport}`;
|
|
187
|
+
}
|
|
157
188
|
const initPath = path.join(appDir, "watchforge-init.tsx");
|
|
158
189
|
|
|
159
190
|
const initContent = `"use client";
|
|
160
191
|
|
|
161
|
-
import {
|
|
162
|
-
import { register } from "@watchforge/browser";
|
|
192
|
+
import { WatchForgeProvider } from "@watchforge/browser/next";
|
|
163
193
|
import { watchforgeConfig } from "${configImport}";
|
|
164
194
|
|
|
165
195
|
export default function WatchForgeInit() {
|
|
166
|
-
|
|
167
|
-
register(watchforgeConfig);
|
|
168
|
-
}, []);
|
|
169
|
-
|
|
170
|
-
return null;
|
|
196
|
+
return <WatchForgeProvider options={watchforgeConfig} />;
|
|
171
197
|
}
|
|
172
198
|
`;
|
|
173
199
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@watchforge/browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
|
-
"
|
|
5
|
+
"types": "./src/index.d.ts",
|
|
6
|
+
"description": "WatchForge JavaScript SDK for browser JavaScript, Next.js, React, Node.js, and Express.js",
|
|
6
7
|
"license": "MIT",
|
|
7
8
|
"keywords": [
|
|
8
9
|
"watchforge",
|
|
@@ -15,12 +16,31 @@
|
|
|
15
16
|
],
|
|
16
17
|
"exports": {
|
|
17
18
|
".": {
|
|
19
|
+
"types": "./src/index.d.ts",
|
|
18
20
|
"browser": "./src/index.js",
|
|
19
21
|
"node": "./src/index.js",
|
|
20
22
|
"default": "./src/index.js"
|
|
21
23
|
},
|
|
22
|
-
"./
|
|
23
|
-
|
|
24
|
+
"./node": {
|
|
25
|
+
"types": "./src/index.d.ts",
|
|
26
|
+
"default": "./src/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./next": {
|
|
29
|
+
"types": "./src/next.d.ts",
|
|
30
|
+
"default": "./src/next.js"
|
|
31
|
+
},
|
|
32
|
+
"./next/server": {
|
|
33
|
+
"types": "./src/next-server.d.ts",
|
|
34
|
+
"default": "./src/next-server.js"
|
|
35
|
+
},
|
|
36
|
+
"./express": {
|
|
37
|
+
"types": "./src/express.d.ts",
|
|
38
|
+
"default": "./src/express.js"
|
|
39
|
+
},
|
|
40
|
+
"./react": {
|
|
41
|
+
"types": "./src/react.d.ts",
|
|
42
|
+
"default": "./src/react.js"
|
|
43
|
+
}
|
|
24
44
|
},
|
|
25
45
|
"bin": {
|
|
26
46
|
"watchforge": "bin/watchforge.js"
|
|
@@ -28,6 +48,7 @@
|
|
|
28
48
|
"files": [
|
|
29
49
|
"bin/**/*.js",
|
|
30
50
|
"src/**/*.js",
|
|
51
|
+
"src/**/*.d.ts",
|
|
31
52
|
"README.md",
|
|
32
53
|
"CONFIGURATION_GUIDE.md",
|
|
33
54
|
"LICENSE"
|
|
@@ -37,9 +58,13 @@
|
|
|
37
58
|
},
|
|
38
59
|
"type": "module",
|
|
39
60
|
"peerDependencies": {
|
|
61
|
+
"express": ">=4.0.0",
|
|
40
62
|
"react": ">=16.8.0"
|
|
41
63
|
},
|
|
42
64
|
"peerDependenciesMeta": {
|
|
65
|
+
"express": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
43
68
|
"react": {
|
|
44
69
|
"optional": true
|
|
45
70
|
}
|
|
@@ -50,8 +75,8 @@
|
|
|
50
75
|
"url": false
|
|
51
76
|
},
|
|
52
77
|
"dependencies": {
|
|
53
|
-
"
|
|
54
|
-
"
|
|
78
|
+
"rrweb": "^2.0.1",
|
|
79
|
+
"source-map-js": "^1.2.1"
|
|
55
80
|
},
|
|
56
81
|
"devDependencies": {
|
|
57
82
|
"rollup": "^4.60.0"
|
package/src/express.d.ts
ADDED
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface WatchForgeRegisterOptions {
|
|
2
|
+
dsn: string;
|
|
3
|
+
app_env?: string;
|
|
4
|
+
release?: string | null;
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
replaysSessionSampleRate?: number;
|
|
7
|
+
replaysOnErrorSampleRate?: number;
|
|
8
|
+
maskAllInputs?: boolean;
|
|
9
|
+
blockClass?: string;
|
|
10
|
+
ignoreClass?: string;
|
|
11
|
+
maskTextClass?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WatchForgeCaptureContext {
|
|
15
|
+
user?: Record<string, unknown>;
|
|
16
|
+
request?: Record<string, unknown>;
|
|
17
|
+
tags?: Record<string, unknown>;
|
|
18
|
+
extra?: Record<string, unknown>;
|
|
19
|
+
contexts?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function register(options: WatchForgeRegisterOptions): void;
|
|
23
|
+
export function init(options: WatchForgeRegisterOptions): void;
|
|
24
|
+
export function captureException(
|
|
25
|
+
error: unknown,
|
|
26
|
+
context?: WatchForgeCaptureContext
|
|
27
|
+
): Promise<void>;
|
|
28
|
+
export function captureMessage(
|
|
29
|
+
message: string,
|
|
30
|
+
level?: string,
|
|
31
|
+
context?: WatchForgeCaptureContext
|
|
32
|
+
): Promise<void>;
|
|
33
|
+
export function addBreadcrumb(breadcrumb: Record<string, unknown>): void;
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
startTransaction,
|
|
37
|
+
getCurrentTransaction,
|
|
38
|
+
finishTransaction,
|
|
39
|
+
Transaction,
|
|
40
|
+
Span,
|
|
41
|
+
} from "./tracing.js";
|
package/src/index.js
CHANGED
|
@@ -13,5 +13,4 @@ export { init } from "./client.js";
|
|
|
13
13
|
export { startTransaction, getCurrentTransaction, finishTransaction, Transaction, Span } from "./tracing.js";
|
|
14
14
|
|
|
15
15
|
// Export framework integrations
|
|
16
|
-
export { expressMiddleware, expressRequestMiddleware } from "./express.js";
|
|
17
|
-
export { ErrorBoundary } from "./react.js";
|
|
16
|
+
export { expressMiddleware, expressRequestMiddleware } from "./express.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WatchForgeCaptureContext,
|
|
3
|
+
WatchForgeRegisterOptions,
|
|
4
|
+
} from "./index.js";
|
|
5
|
+
|
|
6
|
+
export function register(options: WatchForgeRegisterOptions): void;
|
|
7
|
+
|
|
8
|
+
export function captureException(
|
|
9
|
+
error: unknown,
|
|
10
|
+
context?: WatchForgeCaptureContext
|
|
11
|
+
): Promise<void>;
|
|
12
|
+
|
|
13
|
+
export function captureMessage(
|
|
14
|
+
message: string,
|
|
15
|
+
level?: string,
|
|
16
|
+
context?: WatchForgeCaptureContext
|
|
17
|
+
): Promise<void>;
|
|
18
|
+
|
|
19
|
+
export type NextRouteHandler<TContext = unknown> = (
|
|
20
|
+
request: Request,
|
|
21
|
+
context: TContext
|
|
22
|
+
) => Response | Promise<Response>;
|
|
23
|
+
|
|
24
|
+
export function withWatchForgeRouteHandler<TContext = unknown>(
|
|
25
|
+
handler: NextRouteHandler<TContext>
|
|
26
|
+
): NextRouteHandler<TContext>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureException,
|
|
3
|
+
captureMessage,
|
|
4
|
+
register,
|
|
5
|
+
} from "./client.js";
|
|
6
|
+
|
|
7
|
+
export { captureException, captureMessage, register };
|
|
8
|
+
|
|
9
|
+
function sanitizeHeaders(headers) {
|
|
10
|
+
if (!headers || typeof headers.entries !== "function") return {};
|
|
11
|
+
|
|
12
|
+
const sensitive = new Set([
|
|
13
|
+
"authorization",
|
|
14
|
+
"cookie",
|
|
15
|
+
"set-cookie",
|
|
16
|
+
"x-api-key",
|
|
17
|
+
"x-csrftoken",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
return Object.fromEntries(
|
|
21
|
+
Array.from(headers.entries()).filter(
|
|
22
|
+
([key]) => !sensitive.has(key.toLowerCase())
|
|
23
|
+
)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getRequestContext(request) {
|
|
28
|
+
if (!request) return null;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
url: request.url || "",
|
|
32
|
+
method: request.method || "",
|
|
33
|
+
headers: sanitizeHeaders(request.headers),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function withWatchForgeRouteHandler(handler) {
|
|
38
|
+
return async function watchForgeRouteHandler(request, context) {
|
|
39
|
+
try {
|
|
40
|
+
return await handler(request, context);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
await captureException(error, {
|
|
43
|
+
request: getRequestContext(request),
|
|
44
|
+
tags: {
|
|
45
|
+
framework: "nextjs",
|
|
46
|
+
runtime: "server",
|
|
47
|
+
},
|
|
48
|
+
contexts: {
|
|
49
|
+
nextjs: {
|
|
50
|
+
router: "app",
|
|
51
|
+
route_handler: true,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/next.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { WatchForgeRegisterOptions } from "./index.js";
|
|
3
|
+
|
|
4
|
+
export interface WatchForgeProviderProps {
|
|
5
|
+
options: WatchForgeRegisterOptions;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function WatchForgeProvider(
|
|
10
|
+
props: WatchForgeProviderProps
|
|
11
|
+
): ReactElement | null;
|
package/src/next.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect } from "react";
|
|
4
|
+
import { register } from "./client.js";
|
|
5
|
+
|
|
6
|
+
export function WatchForgeProvider({ options, children = null }) {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
register(options);
|
|
9
|
+
}, [options]);
|
|
10
|
+
|
|
11
|
+
return React.createElement(React.Fragment, null, children);
|
|
12
|
+
}
|
package/src/react.d.ts
ADDED
package/src/stacktrace.js
CHANGED
|
@@ -12,8 +12,7 @@ const isNode =
|
|
|
12
12
|
const LIBRARY_PATH_PATTERNS = [
|
|
13
13
|
"/node_modules/",
|
|
14
14
|
"node_modules\\",
|
|
15
|
-
"/webpack/",
|
|
16
|
-
"webpack-internal://",
|
|
15
|
+
"/webpack/runtime/",
|
|
17
16
|
];
|
|
18
17
|
|
|
19
18
|
function isLibraryPath(filePath) {
|
|
@@ -110,6 +109,34 @@ function applySourceContext(frame, sourceLines, lineno) {
|
|
|
110
109
|
.map((l) => l.replace(/\r$/, ""));
|
|
111
110
|
}
|
|
112
111
|
|
|
112
|
+
function normalizeSourcePath(source) {
|
|
113
|
+
if (!source) return "";
|
|
114
|
+
|
|
115
|
+
return String(source)
|
|
116
|
+
.replace(/^webpack-internal:\/\/\/?/, "")
|
|
117
|
+
.replace(/^webpack:\/\/(?:\([^)]*\)\/)?/, "")
|
|
118
|
+
.replace(/^webpack:\/\/[^/]+\//, "")
|
|
119
|
+
.replace(/^\([^)]*\)\//, "")
|
|
120
|
+
.replace(/^file:\/\//, "")
|
|
121
|
+
.split("?")[0]
|
|
122
|
+
.split("#")[0]
|
|
123
|
+
.replace(/\\/g, "/")
|
|
124
|
+
.replace(/^\/+/, "")
|
|
125
|
+
.replace(/^\.\//, "");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function sourceMatchesFrame(source, framePath) {
|
|
129
|
+
const normalizedSource = normalizeSourcePath(source);
|
|
130
|
+
const normalizedFrame = normalizeSourcePath(framePath);
|
|
131
|
+
if (!normalizedSource || !normalizedFrame) return false;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
normalizedSource === normalizedFrame ||
|
|
135
|
+
normalizedSource.endsWith(`/${normalizedFrame}`) ||
|
|
136
|
+
normalizedFrame.endsWith(`/${normalizedSource}`)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
113
140
|
let nodeModulesPromise = null;
|
|
114
141
|
const importNodeBuiltin = (specifier) =>
|
|
115
142
|
new Function("specifier", "return import(specifier)")(specifier);
|
|
@@ -211,10 +238,159 @@ async function fetchBrowserSourceLines(absPath) {
|
|
|
211
238
|
}
|
|
212
239
|
}
|
|
213
240
|
|
|
241
|
+
async function getSourceMapConsumer() {
|
|
242
|
+
try {
|
|
243
|
+
const mod = await import("source-map-js");
|
|
244
|
+
return mod.SourceMapConsumer || mod.default?.SourceMapConsumer || null;
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getSourceMappingUrl(sourceText) {
|
|
251
|
+
const match = sourceText.match(/\/\/# sourceMappingURL=([^\s]+)\s*$/m);
|
|
252
|
+
return match?.[1] || null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function fetchText(url) {
|
|
256
|
+
const response = await fetch(url, { credentials: "same-origin" });
|
|
257
|
+
if (!response.ok) return null;
|
|
258
|
+
const text = await response.text();
|
|
259
|
+
return text.length <= MAX_SOURCE_FILE_BYTES * 10 ? text : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function fetchBrowserSourceMap(absPath) {
|
|
263
|
+
if (!isBrowser || !absPath) return null;
|
|
264
|
+
if (!absPath.startsWith("http://") && !absPath.startsWith("https://") && !absPath.startsWith("/")) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const scriptUrl = new URL(absPath, window.location.href);
|
|
270
|
+
if (scriptUrl.origin !== window.location.origin) return null;
|
|
271
|
+
|
|
272
|
+
const scriptText = await fetchText(scriptUrl.href);
|
|
273
|
+
if (!scriptText) return null;
|
|
274
|
+
|
|
275
|
+
const sourceMappingUrl = getSourceMappingUrl(scriptText);
|
|
276
|
+
if (!sourceMappingUrl) return null;
|
|
277
|
+
|
|
278
|
+
const mapUrl = new URL(sourceMappingUrl, scriptUrl.href);
|
|
279
|
+
if (mapUrl.origin !== window.location.origin && !sourceMappingUrl.startsWith("data:")) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (sourceMappingUrl.startsWith("data:")) {
|
|
284
|
+
const encoded = sourceMappingUrl.split(",", 2)[1];
|
|
285
|
+
if (!encoded) return null;
|
|
286
|
+
const decoded = decodeURIComponent(escape(atob(encoded)));
|
|
287
|
+
return JSON.parse(decoded);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const mapText = await fetchText(mapUrl.href);
|
|
291
|
+
return mapText ? JSON.parse(mapText) : null;
|
|
292
|
+
} catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function enrichFrameWithSourceMap(frame) {
|
|
298
|
+
const sourceMap = await fetchBrowserSourceMap(frame.abs_path);
|
|
299
|
+
if (!sourceMap) return false;
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const SourceMapConsumer = await getSourceMapConsumer();
|
|
303
|
+
if (!SourceMapConsumer) return false;
|
|
304
|
+
|
|
305
|
+
const consumer = await new SourceMapConsumer(sourceMap);
|
|
306
|
+
let source = null;
|
|
307
|
+
let lineno = frame.lineno;
|
|
308
|
+
let colno = frame.colno;
|
|
309
|
+
|
|
310
|
+
if (frame.lineno && frame.colno != null) {
|
|
311
|
+
const original = consumer.originalPositionFor({
|
|
312
|
+
line: frame.lineno,
|
|
313
|
+
column: frame.colno,
|
|
314
|
+
});
|
|
315
|
+
if (original?.source && original?.line) {
|
|
316
|
+
source = original.source;
|
|
317
|
+
lineno = original.line;
|
|
318
|
+
colno = original.column ?? frame.colno;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!source) {
|
|
323
|
+
source = sourceMap.sources?.find((candidate) =>
|
|
324
|
+
sourceMatchesFrame(candidate, frame.abs_path)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!source || !lineno) {
|
|
329
|
+
consumer.destroy?.();
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const content =
|
|
334
|
+
consumer.sourceContentFor?.(source, true) ||
|
|
335
|
+
sourceMap.sourcesContent?.[sourceMap.sources.indexOf(source)];
|
|
336
|
+
|
|
337
|
+
consumer.destroy?.();
|
|
338
|
+
|
|
339
|
+
if (!content) return false;
|
|
340
|
+
|
|
341
|
+
frame.abs_path = source;
|
|
342
|
+
frame.filename = normalizeSourcePath(source).split("/").pop() || frame.filename;
|
|
343
|
+
frame.lineno = lineno;
|
|
344
|
+
frame.colno = colno;
|
|
345
|
+
applySourceContext(frame, content.split("\n"), lineno);
|
|
346
|
+
return Boolean(frame.context_line);
|
|
347
|
+
} catch {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getSourceContentFromWebpackFrame(frame) {
|
|
353
|
+
const globalObject = typeof globalThis !== "undefined" ? globalThis : window;
|
|
354
|
+
const webpackChunks =
|
|
355
|
+
Object.values(globalObject).find(
|
|
356
|
+
(value) =>
|
|
357
|
+
Array.isArray(value) &&
|
|
358
|
+
value.some((item) => Array.isArray(item) && item.length >= 2)
|
|
359
|
+
) || [];
|
|
360
|
+
|
|
361
|
+
const framePath = normalizeSourcePath(frame.abs_path);
|
|
362
|
+
if (!framePath || !Array.isArray(webpackChunks)) return null;
|
|
363
|
+
|
|
364
|
+
for (const chunk of webpackChunks) {
|
|
365
|
+
const modules = Array.isArray(chunk) ? chunk[1] : null;
|
|
366
|
+
if (!modules || typeof modules !== "object") continue;
|
|
367
|
+
|
|
368
|
+
for (const [moduleId, factory] of Object.entries(modules)) {
|
|
369
|
+
if (!sourceMatchesFrame(moduleId, framePath)) continue;
|
|
370
|
+
const source = String(factory);
|
|
371
|
+
return source.includes("\n") ? source.split("\n") : null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
214
378
|
async function enrichFrameWithBrowserSource(frame) {
|
|
215
379
|
if (!frame.in_app || !frame.lineno) return;
|
|
380
|
+
|
|
381
|
+
const webpackLines = getSourceContentFromWebpackFrame(frame);
|
|
382
|
+
if (webpackLines) {
|
|
383
|
+
applySourceContext(frame, webpackLines, frame.lineno);
|
|
384
|
+
if (frame.context_line) return;
|
|
385
|
+
}
|
|
386
|
+
|
|
216
387
|
const lines = await fetchBrowserSourceLines(frame.abs_path);
|
|
217
|
-
if (lines)
|
|
388
|
+
if (lines) {
|
|
389
|
+
applySourceContext(frame, lines, frame.lineno);
|
|
390
|
+
if (frame.context_line) return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
await enrichFrameWithSourceMap(frame);
|
|
218
394
|
}
|
|
219
395
|
|
|
220
396
|
export async function enrichStacktraceAsync(stacktrace) {
|
package/src/tracing.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface Span {
|
|
2
|
+
span_id?: string;
|
|
3
|
+
op?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
start_timestamp?: string;
|
|
6
|
+
finish_timestamp?: string | null;
|
|
7
|
+
duration_ms?: number;
|
|
8
|
+
status?: string;
|
|
9
|
+
data?: Record<string, unknown>;
|
|
10
|
+
tags?: Record<string, unknown>;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Transaction {
|
|
15
|
+
trace_id?: string;
|
|
16
|
+
transaction?: string;
|
|
17
|
+
transaction_name?: string;
|
|
18
|
+
op?: string;
|
|
19
|
+
status?: string;
|
|
20
|
+
spans?: Span[];
|
|
21
|
+
data?: Record<string, unknown>;
|
|
22
|
+
tags?: Record<string, unknown>;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function initTracing(
|
|
27
|
+
dsn: string,
|
|
28
|
+
environment?: string,
|
|
29
|
+
debug?: boolean
|
|
30
|
+
): void;
|
|
31
|
+
|
|
32
|
+
export function startTransaction(
|
|
33
|
+
transaction: string,
|
|
34
|
+
transactionName?: string,
|
|
35
|
+
op?: string
|
|
36
|
+
): Transaction;
|
|
37
|
+
|
|
38
|
+
export function getCurrentTransaction(): Transaction | null;
|
|
39
|
+
export function finishTransaction(status?: string): void;
|