@watchforge/browser 0.1.1 → 0.1.4

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.
@@ -68,6 +68,36 @@ After installation, use it the same way:
68
68
  import { register, ErrorBoundary } from '@watchforge/browser';
69
69
  ```
70
70
 
71
+ ### Next.js Wizard Setup
72
+
73
+ From your Next.js project root:
74
+
75
+ ```bash
76
+ npx @watchforge/browser -i nextjs --dsn "https://PUBLIC_KEY@dev.watchforges.com/PROJECT_ID" --app-env production
77
+ ```
78
+
79
+ With error-triggered Session Replay:
80
+
81
+ ```bash
82
+ npx @watchforge/browser -i nextjs \
83
+ --dsn "https://PUBLIC_KEY@dev.watchforges.com/PROJECT_ID" \
84
+ --app-env production \
85
+ --replays-on-error 1
86
+ ```
87
+
88
+ The wizard:
89
+
90
+ 1. Installs `@watchforge/browser`
91
+ 2. Creates `watchforge.config.ts`
92
+ 3. Creates `app/watchforge-init.tsx` (or `src/app/watchforge-init.tsx`)
93
+ 4. Patches `app/layout.tsx` to render `<WatchForgeInit />`
94
+
95
+ If you only want file generation and no package install:
96
+
97
+ ```bash
98
+ npx @watchforge/browser -i nextjs --dsn "..." --skip-install
99
+ ```
100
+
71
101
  ## Quick Testing
72
102
 
73
103
  ### Prerequisites
@@ -710,7 +740,11 @@ try {
710
740
  ```javascript
711
741
  // server.js
712
742
  const express = require('express');
713
- const { register, expressMiddleware } = require('@watchforge/browser');
743
+ const {
744
+ register,
745
+ expressRequestMiddleware,
746
+ expressMiddleware,
747
+ } = require('@watchforge/browser');
714
748
 
715
749
  const app = express();
716
750
 
@@ -719,6 +753,11 @@ register({
719
753
  app_env: process.env.NODE_ENV,
720
754
  });
721
755
 
756
+ app.use(express.json());
757
+ app.use(expressRequestMiddleware());
758
+
759
+ // routes here
760
+
722
761
  // Add error handler (must be last middleware)
723
762
  app.use(expressMiddleware());
724
763
 
@@ -735,6 +774,11 @@ import App from './App';
735
774
  register({
736
775
  dsn: process.env.REACT_APP_WATCHFORGE_DSN,
737
776
  app_env: process.env.NODE_ENV,
777
+ // Optional: upload the last 60s of masked browser replay events when an error occurs.
778
+ replaysOnErrorSampleRate: 1.0,
779
+ // Optional: continuously sample full sessions. Keep 0 in production unless you need it.
780
+ replaysSessionSampleRate: 0,
781
+ maskAllInputs: true,
738
782
  });
739
783
 
740
784
  ReactDOM.render(
@@ -745,6 +789,95 @@ ReactDOM.render(
745
789
  );
746
790
  ```
747
791
 
792
+ ## Stack Trace Source Context
793
+
794
+ The SDK enriches stack frames with Python-style source context when possible:
795
+
796
+ | Field | Description |
797
+ |-------|-------------|
798
+ | `context_line` | The exact line where the error occurred |
799
+ | `pre_context` | Up to 5 lines before the error |
800
+ | `post_context` | Up to 5 lines after the error |
801
+
802
+ ### Node.js / Express
803
+
804
+ For local project files, the SDK reads source from disk and attaches context lines automatically. No extra configuration is required.
805
+
806
+ ### Browser (development)
807
+
808
+ In dev builds (Next.js, Vite, etc.), the SDK attempts to fetch same-origin script URLs from the stack trace and slice source lines around the reported line number.
809
+
810
+ Works when stack frames point to readable URLs such as:
811
+
812
+ ```txt
813
+ http://localhost:3000/_next/static/chunks/...
814
+ http://localhost:3000/src/App.tsx
815
+ ```
816
+
817
+ ### Browser (production / minified bundles)
818
+
819
+ Production bundles often report locations like:
820
+
821
+ ```txt
822
+ https://your-app.com/_next/static/chunks/app-abc123.js:1:98432
823
+ ```
824
+
825
+ The SDK still sends filename, line, and column, but **cannot** show original TypeScript/JSX source without **source maps**.
826
+
827
+ **Current status:** source map upload and symbolication are not yet supported (planned for a future release).
828
+
829
+ **Workarounds today:**
830
+
831
+ 1. Test with `npm run dev` / unminified builds to see source lines in WatchForge.
832
+ 2. Use `release` in `register()` so issues are grouped by deploy version.
833
+ 3. Rely on breadcrumbs and the Browser Event Summary in the dashboard to understand user actions before the error.
834
+
835
+ When source map support ships, you will upload maps during deploy and WatchForge will resolve minified frames back to original files.
836
+
837
+ ## Session Replay
838
+
839
+ The browser SDK can record a Sentry-style DOM replay using `rrweb`. Replays are **not videos**; they are masked DOM snapshots and incremental browser events that WatchForge can play back in the Issue Detail page.
840
+
841
+ Enable replay capture when registering the SDK:
842
+
843
+ ```javascript
844
+ register({
845
+ dsn: "https://PUBLIC_KEY@dev.watchforges.com/PROJECT_ID",
846
+ app_env: "production",
847
+ replaysOnErrorSampleRate: 1.0,
848
+ replaysSessionSampleRate: 0,
849
+ maskAllInputs: true,
850
+ });
851
+ ```
852
+
853
+ Replay behavior:
854
+
855
+ - `replaysOnErrorSampleRate`: records in buffer mode and uploads the last 60 seconds when an error is captured.
856
+ - `replaysSessionSampleRate`: continuously samples full browser sessions.
857
+ - Text/input privacy is handled in the browser before upload.
858
+ - Password, email, tel and text inputs are masked when `maskAllInputs` is true.
859
+ - Elements with `.rr-block` are blocked.
860
+ - Elements with `.rr-ignore` ignore input events.
861
+ - Elements with `.rr-mask` mask text.
862
+
863
+ When an error is captured, the SDK attaches:
864
+
865
+ ```json
866
+ {
867
+ "replay_id": "...",
868
+ "session_id": "...",
869
+ "contexts": {
870
+ "replay": {
871
+ "replay_id": "...",
872
+ "session_id": "...",
873
+ "sampled": true
874
+ }
875
+ }
876
+ }
877
+ ```
878
+
879
+ The dashboard shows the linked replay in the Issue Detail **Session Replay** tab.
880
+
748
881
  ## Test Scripts
749
882
 
750
883
  Test scripts are included in the SDK directory:
package/README.md CHANGED
@@ -18,6 +18,25 @@ Browser and Node SDK for WatchForge. **One call to `register()`** turns on autom
18
18
  npm install @watchforge/browser
19
19
  ```
20
20
 
21
+ ## Next.js one-line setup
22
+
23
+ For Next.js apps, run the setup wizard from your app root:
24
+
25
+ ```bash
26
+ npx @watchforge/browser -i nextjs --dsn "https://PUBLIC_KEY@your-host/PROJECT_ID" --app-env production
27
+ ```
28
+
29
+ With Session Replay on errors:
30
+
31
+ ```bash
32
+ npx @watchforge/browser -i nextjs \
33
+ --dsn "https://PUBLIC_KEY@your-host/PROJECT_ID" \
34
+ --app-env production \
35
+ --replays-on-error 1
36
+ ```
37
+
38
+ The wizard installs `@watchforge/browser`, writes `watchforge.config.ts`, creates a client init component, and patches `app/layout.tsx` or `pages/_app.tsx`.
39
+
21
40
  ## Quick start (all most apps need)
22
41
 
23
42
  Call **`register()` once** as early as possible (e.g. app entry / main bundle), with your **DSN** from the WatchForge project settings.
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+
8
+ const HELP = `
9
+ WatchForge setup wizard
10
+
11
+ Usage:
12
+ npx @watchforge/browser -i nextjs --dsn <dsn> [options]
13
+ npx @watchforge/browser init nextjs --dsn <dsn> [options]
14
+
15
+ Options:
16
+ -i, --integration <name> Framework integration. Currently: nextjs
17
+ --dsn <dsn> WatchForge DSN
18
+ --app-env <env> App environment (default: production)
19
+ --debug Enable SDK debug logging
20
+ --replays-on-error <rate> Upload replay when errors happen (default: 0)
21
+ --replays-session <rate> Continuously sample sessions (default: 0)
22
+ --skip-install Do not install @watchforge/browser
23
+ -h, --help Show help
24
+
25
+ Example:
26
+ npx @watchforge/browser -i nextjs --dsn "https://PUBLIC_KEY@dev.watchforges.com/PROJECT_ID" --app-env development --replays-on-error 1
27
+ `;
28
+
29
+ function parseArgs(argv) {
30
+ const args = {
31
+ integration: null,
32
+ dsn: null,
33
+ appEnv: "production",
34
+ debug: false,
35
+ replaysOnError: "0",
36
+ replaysSession: "0",
37
+ skipInstall: false,
38
+ help: false,
39
+ };
40
+
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const arg = argv[i];
43
+ const next = argv[i + 1];
44
+
45
+ if (arg === "-h" || arg === "--help") args.help = true;
46
+ else if (arg === "-i" || arg === "--integration") {
47
+ args.integration = next;
48
+ i++;
49
+ } else if (arg === "init" || arg === "setup") {
50
+ args.integration = argv[i + 1] || args.integration;
51
+ i++;
52
+ } else if (arg === "--dsn") {
53
+ args.dsn = next;
54
+ i++;
55
+ } else if (arg === "--app-env" || arg === "--environment") {
56
+ args.appEnv = next || args.appEnv;
57
+ i++;
58
+ } else if (arg === "--debug") {
59
+ args.debug = true;
60
+ } else if (arg === "--replays-on-error") {
61
+ args.replaysOnError = next || "0";
62
+ i++;
63
+ } else if (arg === "--replays-session") {
64
+ args.replaysSession = next || "0";
65
+ i++;
66
+ } else if (arg === "--skip-install") {
67
+ args.skipInstall = true;
68
+ }
69
+ }
70
+
71
+ return args;
72
+ }
73
+
74
+ function log(message) {
75
+ console.log(`WatchForge: ${message}`);
76
+ }
77
+
78
+ function fail(message) {
79
+ console.error(`WatchForge setup failed: ${message}`);
80
+ process.exit(1);
81
+ }
82
+
83
+ function fileExists(filePath) {
84
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
85
+ }
86
+
87
+ function findNextLayout(cwd) {
88
+ const candidates = [
89
+ path.join(cwd, "src", "app", "layout.tsx"),
90
+ path.join(cwd, "src", "app", "layout.jsx"),
91
+ path.join(cwd, "app", "layout.tsx"),
92
+ path.join(cwd, "app", "layout.jsx"),
93
+ ];
94
+ return candidates.find(fileExists) || null;
95
+ }
96
+
97
+ function findPagesApp(cwd) {
98
+ const candidates = [
99
+ path.join(cwd, "src", "pages", "_app.tsx"),
100
+ path.join(cwd, "src", "pages", "_app.jsx"),
101
+ path.join(cwd, "pages", "_app.tsx"),
102
+ path.join(cwd, "pages", "_app.jsx"),
103
+ ];
104
+ return candidates.find(fileExists) || null;
105
+ }
106
+
107
+ function writeIfChanged(filePath, content) {
108
+ if (fileExists(filePath) && fs.readFileSync(filePath, "utf8") === content) {
109
+ return false;
110
+ }
111
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
112
+ fs.writeFileSync(filePath, content);
113
+ return true;
114
+ }
115
+
116
+ function createConfig(cwd, args) {
117
+ const configPath = path.join(cwd, "watchforge.config.ts");
118
+ const content = `export const watchforgeConfig = {
119
+ dsn: ${JSON.stringify(args.dsn)},
120
+ app_env: ${JSON.stringify(args.appEnv)},
121
+ debug: ${args.debug ? "true" : "false"},
122
+ replaysOnErrorSampleRate: ${Number(args.replaysOnError)},
123
+ replaysSessionSampleRate: ${Number(args.replaysSession)},
124
+ maskAllInputs: true,
125
+ };
126
+ `;
127
+ writeIfChanged(configPath, content);
128
+ log(`wrote ${path.relative(cwd, configPath)}`);
129
+ }
130
+
131
+ function installPackage(cwd, skipInstall) {
132
+ if (skipInstall) {
133
+ log("skipped npm install");
134
+ return;
135
+ }
136
+
137
+ const hasPnpm = fileExists(path.join(cwd, "pnpm-lock.yaml"));
138
+ const hasYarn = fileExists(path.join(cwd, "yarn.lock"));
139
+ const hasBun = fileExists(path.join(cwd, "bun.lockb"));
140
+
141
+ const command = hasPnpm
142
+ ? "pnpm add @watchforge/browser"
143
+ : hasYarn
144
+ ? "yarn add @watchforge/browser"
145
+ : hasBun
146
+ ? "bun add @watchforge/browser"
147
+ : "npm install @watchforge/browser";
148
+
149
+ log(`installing package: ${command}`);
150
+ execSync(command, { cwd, stdio: "inherit" });
151
+ }
152
+
153
+ function patchAppRouter(cwd, layoutPath) {
154
+ const appDir = path.dirname(layoutPath);
155
+ const isSrcApp = appDir.endsWith(path.join("src", "app"));
156
+ const configImport = isSrcApp ? "../../watchforge.config" : "../watchforge.config";
157
+ const initPath = path.join(appDir, "watchforge-init.tsx");
158
+
159
+ const initContent = `"use client";
160
+
161
+ import { useEffect } from "react";
162
+ import { register } from "@watchforge/browser";
163
+ import { watchforgeConfig } from "${configImport}";
164
+
165
+ export default function WatchForgeInit() {
166
+ useEffect(() => {
167
+ register(watchforgeConfig);
168
+ }, []);
169
+
170
+ return null;
171
+ }
172
+ `;
173
+
174
+ writeIfChanged(initPath, initContent);
175
+ log(`wrote ${path.relative(cwd, initPath)}`);
176
+
177
+ let layout = fs.readFileSync(layoutPath, "utf8");
178
+ if (!layout.includes("WatchForgeInit")) {
179
+ layout = `import WatchForgeInit from "./watchforge-init";\n${layout}`;
180
+ }
181
+
182
+ if (!layout.includes("<WatchForgeInit />")) {
183
+ layout = layout.replace(/<body([^>]*)>/, "<body$1>\n <WatchForgeInit />\n ");
184
+ }
185
+
186
+ fs.writeFileSync(layoutPath, layout);
187
+ log(`patched ${path.relative(cwd, layoutPath)}`);
188
+ }
189
+
190
+ function patchPagesRouter(cwd, appPath) {
191
+ const pagesDir = path.dirname(appPath);
192
+ const isSrcPages = pagesDir.endsWith(path.join("src", "pages"));
193
+ const configImport = isSrcPages ? "../../watchforge.config" : "../watchforge.config";
194
+ const content = fs.readFileSync(appPath, "utf8");
195
+
196
+ if (content.includes("register(watchforgeConfig)")) {
197
+ log(`${path.relative(cwd, appPath)} already contains WatchForge setup`);
198
+ return;
199
+ }
200
+
201
+ const patched = `import { useEffect } from "react";
202
+ import { register } from "@watchforge/browser";
203
+ import { watchforgeConfig } from "${configImport}";
204
+
205
+ ${content.replace(
206
+ /function\s+App\s*\(([^)]*)\)\s*{/,
207
+ "function App($1) {\n useEffect(() => {\n register(watchforgeConfig);\n }, []);"
208
+ )}`;
209
+
210
+ fs.writeFileSync(appPath, patched);
211
+ log(`patched ${path.relative(cwd, appPath)}`);
212
+ }
213
+
214
+ function initNextjs(args) {
215
+ const cwd = process.cwd();
216
+ if (!fileExists(path.join(cwd, "package.json"))) {
217
+ fail("run this command from the root of your Next.js project");
218
+ }
219
+
220
+ createConfig(cwd, args);
221
+ installPackage(cwd, args.skipInstall);
222
+
223
+ const layoutPath = findNextLayout(cwd);
224
+ if (layoutPath) {
225
+ patchAppRouter(cwd, layoutPath);
226
+ return;
227
+ }
228
+
229
+ const appPath = findPagesApp(cwd);
230
+ if (appPath) {
231
+ patchPagesRouter(cwd, appPath);
232
+ return;
233
+ }
234
+
235
+ fail("could not find app/layout.tsx or pages/_app.tsx");
236
+ }
237
+
238
+ const args = parseArgs(process.argv.slice(2));
239
+
240
+ if (args.help) {
241
+ console.log(HELP);
242
+ process.exit(0);
243
+ }
244
+
245
+ if (!args.integration) {
246
+ fail("missing integration. Use: -i nextjs");
247
+ }
248
+
249
+ if (args.integration !== "nextjs") {
250
+ fail(`unsupported integration "${args.integration}". Currently supported: nextjs`);
251
+ }
252
+
253
+ if (!args.dsn) {
254
+ fail("missing --dsn");
255
+ }
256
+
257
+ initNextjs(args);
258
+ log("setup complete");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@watchforge/browser",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "main": "./src/index.js",
5
5
  "description": "WatchForge JavaScript SDK for Node.js, Express.js, and React",
6
6
  "license": "MIT",
@@ -22,12 +22,19 @@
22
22
  "./express": "./src/express.js",
23
23
  "./react": "./src/react.js"
24
24
  },
25
+ "bin": {
26
+ "watchforge": "bin/watchforge.js"
27
+ },
25
28
  "files": [
29
+ "bin/**/*.js",
26
30
  "src/**/*.js",
27
31
  "README.md",
28
32
  "CONFIGURATION_GUIDE.md",
29
33
  "LICENSE"
30
34
  ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
31
38
  "type": "module",
32
39
  "peerDependencies": {
33
40
  "react": ">=16.8.0"
@@ -43,7 +50,8 @@
43
50
  "url": false
44
51
  },
45
52
  "dependencies": {
46
- "express": "^5.2.1"
53
+ "express": "^5.2.1",
54
+ "rrweb": "^2.0.1"
47
55
  },
48
56
  "devDependencies": {
49
57
  "rollup": "^4.60.0"