@watchforge/browser 0.1.3 → 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.
@@ -0,0 +1,284 @@
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 commandExists(command) {
88
+ try {
89
+ execSync(`command -v ${command}`, { stdio: "ignore" });
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ function findNextLayout(cwd) {
97
+ const candidates = [
98
+ path.join(cwd, "src", "app", "layout.tsx"),
99
+ path.join(cwd, "src", "app", "layout.jsx"),
100
+ path.join(cwd, "app", "layout.tsx"),
101
+ path.join(cwd, "app", "layout.jsx"),
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
+
121
+ return candidates.find(fileExists) || null;
122
+ }
123
+
124
+ function findPagesApp(cwd) {
125
+ const candidates = [
126
+ path.join(cwd, "src", "pages", "_app.tsx"),
127
+ path.join(cwd, "src", "pages", "_app.jsx"),
128
+ path.join(cwd, "pages", "_app.tsx"),
129
+ path.join(cwd, "pages", "_app.jsx"),
130
+ ];
131
+ return candidates.find(fileExists) || null;
132
+ }
133
+
134
+ function writeIfChanged(filePath, content) {
135
+ if (fileExists(filePath) && fs.readFileSync(filePath, "utf8") === content) {
136
+ return false;
137
+ }
138
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
139
+ fs.writeFileSync(filePath, content);
140
+ return true;
141
+ }
142
+
143
+ function createConfig(cwd, args) {
144
+ const configPath = path.join(cwd, "watchforge.config.ts");
145
+ const content = `export const watchforgeConfig = {
146
+ dsn: ${JSON.stringify(args.dsn)},
147
+ app_env: ${JSON.stringify(args.appEnv)},
148
+ debug: ${args.debug ? "true" : "false"},
149
+ replaysOnErrorSampleRate: ${Number(args.replaysOnError)},
150
+ replaysSessionSampleRate: ${Number(args.replaysSession)},
151
+ maskAllInputs: true,
152
+ };
153
+ `;
154
+ writeIfChanged(configPath, content);
155
+ log(`wrote ${path.relative(cwd, configPath)}`);
156
+ }
157
+
158
+ function installPackage(cwd, skipInstall) {
159
+ if (skipInstall) {
160
+ log("skipped npm install");
161
+ return;
162
+ }
163
+
164
+ const hasPnpm = fileExists(path.join(cwd, "pnpm-lock.yaml"));
165
+ const hasYarn = fileExists(path.join(cwd, "yarn.lock"));
166
+ const hasBun = fileExists(path.join(cwd, "bun.lockb"));
167
+
168
+ const command = hasPnpm && commandExists("pnpm")
169
+ ? "pnpm add @watchforge/browser"
170
+ : hasYarn && commandExists("yarn")
171
+ ? "yarn add @watchforge/browser"
172
+ : hasBun && commandExists("bun")
173
+ ? "bun add @watchforge/browser"
174
+ : "npm install @watchforge/browser";
175
+
176
+ log(`installing package: ${command}`);
177
+ execSync(command, { cwd, stdio: "inherit" });
178
+ }
179
+
180
+ function patchAppRouter(cwd, layoutPath) {
181
+ const appDir = path.dirname(layoutPath);
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
+ }
188
+ const initPath = path.join(appDir, "watchforge-init.tsx");
189
+
190
+ const initContent = `"use client";
191
+
192
+ import { WatchForgeProvider } from "@watchforge/browser/next";
193
+ import { watchforgeConfig } from "${configImport}";
194
+
195
+ export default function WatchForgeInit() {
196
+ return <WatchForgeProvider options={watchforgeConfig} />;
197
+ }
198
+ `;
199
+
200
+ writeIfChanged(initPath, initContent);
201
+ log(`wrote ${path.relative(cwd, initPath)}`);
202
+
203
+ let layout = fs.readFileSync(layoutPath, "utf8");
204
+ if (!layout.includes("WatchForgeInit")) {
205
+ layout = `import WatchForgeInit from "./watchforge-init";\n${layout}`;
206
+ }
207
+
208
+ if (!layout.includes("<WatchForgeInit />")) {
209
+ layout = layout.replace(/<body([^>]*)>/, "<body$1>\n <WatchForgeInit />\n ");
210
+ }
211
+
212
+ fs.writeFileSync(layoutPath, layout);
213
+ log(`patched ${path.relative(cwd, layoutPath)}`);
214
+ }
215
+
216
+ function patchPagesRouter(cwd, appPath) {
217
+ const pagesDir = path.dirname(appPath);
218
+ const isSrcPages = pagesDir.endsWith(path.join("src", "pages"));
219
+ const configImport = isSrcPages ? "../../watchforge.config" : "../watchforge.config";
220
+ const content = fs.readFileSync(appPath, "utf8");
221
+
222
+ if (content.includes("register(watchforgeConfig)")) {
223
+ log(`${path.relative(cwd, appPath)} already contains WatchForge setup`);
224
+ return;
225
+ }
226
+
227
+ const patched = `import { useEffect } from "react";
228
+ import { register } from "@watchforge/browser";
229
+ import { watchforgeConfig } from "${configImport}";
230
+
231
+ ${content.replace(
232
+ /function\s+App\s*\(([^)]*)\)\s*{/,
233
+ "function App($1) {\n useEffect(() => {\n register(watchforgeConfig);\n }, []);"
234
+ )}`;
235
+
236
+ fs.writeFileSync(appPath, patched);
237
+ log(`patched ${path.relative(cwd, appPath)}`);
238
+ }
239
+
240
+ function initNextjs(args) {
241
+ const cwd = process.cwd();
242
+ if (!fileExists(path.join(cwd, "package.json"))) {
243
+ fail("run this command from the root of your Next.js project");
244
+ }
245
+
246
+ createConfig(cwd, args);
247
+ installPackage(cwd, args.skipInstall);
248
+
249
+ const layoutPath = findNextLayout(cwd);
250
+ if (layoutPath) {
251
+ patchAppRouter(cwd, layoutPath);
252
+ return;
253
+ }
254
+
255
+ const appPath = findPagesApp(cwd);
256
+ if (appPath) {
257
+ patchPagesRouter(cwd, appPath);
258
+ return;
259
+ }
260
+
261
+ fail("could not find app/layout.tsx or pages/_app.tsx");
262
+ }
263
+
264
+ const args = parseArgs(process.argv.slice(2));
265
+
266
+ if (args.help) {
267
+ console.log(HELP);
268
+ process.exit(0);
269
+ }
270
+
271
+ if (!args.integration) {
272
+ fail("missing integration. Use: -i nextjs");
273
+ }
274
+
275
+ if (args.integration !== "nextjs") {
276
+ fail(`unsupported integration "${args.integration}". Currently supported: nextjs`);
277
+ }
278
+
279
+ if (!args.dsn) {
280
+ fail("missing --dsn");
281
+ }
282
+
283
+ initNextjs(args);
284
+ log("setup complete");
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@watchforge/browser",
3
- "version": "0.1.3",
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,24 +16,55 @@
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
+ }
44
+ },
45
+ "bin": {
46
+ "watchforge": "bin/watchforge.js"
24
47
  },
25
48
  "files": [
49
+ "bin/**/*.js",
26
50
  "src/**/*.js",
51
+ "src/**/*.d.ts",
27
52
  "README.md",
28
53
  "CONFIGURATION_GUIDE.md",
29
54
  "LICENSE"
30
55
  ],
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
31
59
  "type": "module",
32
60
  "peerDependencies": {
61
+ "express": ">=4.0.0",
33
62
  "react": ">=16.8.0"
34
63
  },
35
64
  "peerDependenciesMeta": {
65
+ "express": {
66
+ "optional": true
67
+ },
36
68
  "react": {
37
69
  "optional": true
38
70
  }
@@ -43,7 +75,8 @@
43
75
  "url": false
44
76
  },
45
77
  "dependencies": {
46
- "express": "^5.2.1"
78
+ "rrweb": "^2.0.1",
79
+ "source-map-js": "^1.2.1"
47
80
  },
48
81
  "devDependencies": {
49
82
  "rollup": "^4.60.0"
package/src/client.js CHANGED
@@ -9,6 +9,11 @@ import {
9
9
  getPerformanceContext,
10
10
  getNodeServerContext,
11
11
  } from "./contexts.js";
12
+ import {
13
+ flushReplayForEvent,
14
+ getReplayContext,
15
+ initReplay,
16
+ } from "./replay.js";
12
17
 
13
18
  let DSN = null;
14
19
  let APP_ENV = "production";
@@ -378,6 +383,12 @@ export function register({
378
383
  app_env = "production",
379
384
  release = null,
380
385
  debug = false,
386
+ replaysSessionSampleRate = 0,
387
+ replaysOnErrorSampleRate = 0,
388
+ maskAllInputs = true,
389
+ blockClass = "rr-block",
390
+ ignoreClass = "rr-ignore",
391
+ maskTextClass = "rr-mask",
381
392
  }) {
382
393
  DSN = dsn;
383
394
  APP_ENV = app_env;
@@ -409,6 +420,15 @@ export function register({
409
420
  // Browser: Set up full instrumentation (errors, breadcrumbs, HTTP, navigation)
410
421
  if (isBrowser) {
411
422
  setupBrowserInstrumentation();
423
+ initReplay({
424
+ replaysSessionSampleRate,
425
+ replaysOnErrorSampleRate,
426
+ maskAllInputs,
427
+ blockClass,
428
+ ignoreClass,
429
+ maskTextClass,
430
+ debug,
431
+ });
412
432
  }
413
433
 
414
434
  // Node.js: Set up process error handlers
@@ -448,6 +468,12 @@ export async function captureException(error, context = {}) {
448
468
  sdk: getSdkMetadata(),
449
469
  };
450
470
 
471
+ const replay = flushReplayForEvent(DSN, event.event_id);
472
+ if (replay) {
473
+ event.replay_id = replay.replay_id;
474
+ event.session_id = replay.session_id;
475
+ }
476
+
451
477
  if (RELEASE) {
452
478
  event.release = RELEASE;
453
479
  }
@@ -487,6 +513,10 @@ export async function captureException(error, context = {}) {
487
513
  }
488
514
 
489
515
  event.contexts = await buildEventContexts(context);
516
+ const replayContext = getReplayContext();
517
+ if (replayContext) {
518
+ event.contexts.replay = replayContext;
519
+ }
490
520
 
491
521
  const bcs = getBreadcrumbsSnapshot();
492
522
  if (bcs.length > 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> {}