@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.
@@ -2,10 +2,51 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- The WatchForge JavaScript SDK automatically captures errors and events in:
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
- - **React** frontend applications
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
- ```javascript
68
- import { register, ErrorBoundary } from '@watchforge/browser';
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/watchforge-init.tsx`)
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
- ```javascript
312
+ ```ts
194
313
  // app.js
195
- const { register, captureException, captureMessage } = require('@watchforge/browser');
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
- ```javascript
340
+ ```ts
218
341
  // server.js
219
- const express = require('express');
220
- const { register, expressMiddleware } = require('@watchforge/browser');
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 and Node SDK for WatchForge. **One call to `register()`** turns on automatic error reporting—no manual `try/catch` required for typical crashes.
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 isSrcApp = appDir.endsWith(path.join("src", "app"));
156
- const configImport = isSrcApp ? "../../watchforge.config" : "../watchforge.config";
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 { useEffect } from "react";
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
- useEffect(() => {
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.4",
3
+ "version": "0.1.5",
4
4
  "main": "./src/index.js",
5
- "description": "WatchForge JavaScript SDK for Node.js, Express.js, and React",
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
- "./express": "./src/express.js",
23
- "./react": "./src/react.js"
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
- "express": "^5.2.1",
54
- "rrweb": "^2.0.1"
78
+ "rrweb": "^2.0.1",
79
+ "source-map-js": "^1.2.1"
55
80
  },
56
81
  "devDependencies": {
57
82
  "rollup": "^4.60.0"
@@ -0,0 +1,12 @@
1
+ export function expressRequestMiddleware(): (
2
+ req: any,
3
+ res: any,
4
+ next: (error?: unknown) => void
5
+ ) => void;
6
+
7
+ export function expressMiddleware(): (
8
+ err: Error,
9
+ req: any,
10
+ res: any,
11
+ next: (error?: unknown) => void
12
+ ) => void;
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
@@ -0,0 +1,8 @@
1
+ import type { Component, ReactNode } from "react";
2
+
3
+ export interface ErrorBoundaryProps {
4
+ children?: ReactNode;
5
+ fallback?: ReactNode;
6
+ }
7
+
8
+ export class ErrorBoundary extends Component<ErrorBoundaryProps> {}
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) applySourceContext(frame, lines, frame.lineno);
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) {
@@ -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;