@watchforge/browser 0.1.10 → 0.1.12

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/bin/watchforge.js CHANGED
@@ -214,6 +214,7 @@ function createConfig(cwd, args) {
214
214
  replaysOnErrorSampleRate: ${Number(args.replaysOnError)},
215
215
  replaysSessionSampleRate: ${Number(args.replaysSession)},
216
216
  maskAllInputs: true,
217
+ projectRoot: process.env.INIT_CWD || process.env.PWD || "",
217
218
  };
218
219
  `;
219
220
  writeIfChanged(configPath, content);
@@ -280,6 +281,7 @@ export default function WatchForgeInit() {
280
281
 
281
282
  function createNextGlobalError(cwd, layoutPath) {
282
283
  const appDir = path.dirname(layoutPath);
284
+ const configImport = toImportPath(appDir, getConfigPath(cwd));
283
285
  const usesTs = projectUsesTypeScript(cwd);
284
286
  const globalErrorPath = path.join(appDir, usesTs ? "global-error.tsx" : "global-error.jsx");
285
287
 
@@ -301,25 +303,31 @@ function createNextGlobalError(cwd, layoutPath) {
301
303
  const content = `"use client";
302
304
 
303
305
  import { useEffect } from "react";
304
- import { captureException } from "@watchforge/browser";
306
+ import { register, captureException } from "@watchforge/browser";
307
+ import { watchforgeConfig } from "${configImport}";
305
308
 
306
309
  export default function GlobalError(${propsSignature}) {
307
310
  useEffect(() => {
308
- void captureException(error, {
309
- tags: {
310
- framework: "nextjs",
311
- runtime: "error-boundary",
312
- },
313
- extra: {
314
- digest: error.digest,
315
- },
316
- contexts: {
317
- nextjs: {
318
- error_boundary: "global-error",
319
- digest: error.digest || null,
311
+ register(watchforgeConfig);
312
+ const timer = window.setTimeout(() => {
313
+ void captureException(error, {
314
+ tags: {
315
+ framework: "nextjs",
316
+ runtime: "error-boundary",
320
317
  },
321
- },
322
- });
318
+ extra: {
319
+ digest: error.digest,
320
+ },
321
+ contexts: {
322
+ nextjs: {
323
+ error_boundary: "global-error",
324
+ digest: error.digest || null,
325
+ },
326
+ },
327
+ });
328
+ }, 100);
329
+
330
+ return () => window.clearTimeout(timer);
323
331
  }, [error]);
324
332
 
325
333
  return (
@@ -347,17 +355,35 @@ function createNextInstrumentation(cwd, layoutPath) {
347
355
  const configImport = toImportPath(path.dirname(instrumentationPath), getConfigPath(cwd));
348
356
 
349
357
  if (fileExists(instrumentationPath)) {
350
- const content = fs.readFileSync(instrumentationPath, "utf8");
358
+ let content = fs.readFileSync(instrumentationPath, "utf8");
359
+ if (content.includes("onRequestError")) {
360
+ log(`${path.relative(cwd, instrumentationPath)} already contains WatchForge request error capture`);
361
+ return;
362
+ }
363
+
351
364
  if (content.includes("@watchforge/browser/next/server")) {
352
- log(`${path.relative(cwd, instrumentationPath)} already contains WatchForge setup`);
365
+ content = content.replace(
366
+ /import\s*\{\s*register\s+as\s+registerWatchForge\s*\}\s*from\s*"@watchforge\/browser\/next\/server";/,
367
+ 'import { register as registerWatchForge, onRequestError as watchForgeOnRequestError } from "@watchforge/browser/next/server";'
368
+ );
369
+ if (!content.includes("watchForgeOnRequestError")) {
370
+ content = `import { onRequestError as watchForgeOnRequestError } from "@watchforge/browser/next/server";\n${content}`;
371
+ }
372
+ content = `${content.trim()}\n\nexport const onRequestError = watchForgeOnRequestError;\n`;
373
+ fs.writeFileSync(instrumentationPath, content);
374
+ log(`patched ${path.relative(cwd, instrumentationPath)} for SSR/request error capture`);
353
375
  return;
354
376
  }
377
+
355
378
  log(`skipped ${path.relative(cwd, instrumentationPath)} because it already exists`);
356
- log("add registerWatchForge(watchforgeConfig) there manually to report Next.js server runtime errors");
379
+ log("add registerWatchForge(watchforgeConfig) and onRequestError there manually");
357
380
  return;
358
381
  }
359
382
 
360
- const content = `import { register as registerWatchForge } from "@watchforge/browser/next/server";
383
+ const content = `import {
384
+ register as registerWatchForge,
385
+ onRequestError as watchForgeOnRequestError,
386
+ } from "@watchforge/browser/next/server";
361
387
  import { watchforgeConfig } from "${configImport}";
362
388
 
363
389
  export async function register() {
@@ -365,6 +391,8 @@ export async function register() {
365
391
  registerWatchForge(watchforgeConfig);
366
392
  }
367
393
  }
394
+
395
+ export const onRequestError = watchForgeOnRequestError;
368
396
  `;
369
397
 
370
398
  writeIfChanged(instrumentationPath, content);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@watchforge/browser",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "main": "./src/index.js",
5
5
  "types": "./src/index.d.ts",
6
6
  "description": "WatchForge JavaScript SDK for browser JavaScript, Next.js, React, Node.js, and Express.js",
package/src/client.js CHANGED
@@ -2,6 +2,7 @@ import { sendEvent, parseDsn } from "./transport.js";
2
2
  import {
3
3
  buildStacktraceFromError,
4
4
  enrichStacktraceAsync,
5
+ setProjectRoot,
5
6
  } from "./stacktrace.js";
6
7
  import {
7
8
  getSdkMetadata,
@@ -13,12 +14,14 @@ import {
13
14
  flushReplayForEvent,
14
15
  getReplayContext,
15
16
  initReplay,
17
+ waitForReplayReady,
16
18
  } from "./replay.js";
17
19
 
18
20
  let DSN = null;
19
21
  let APP_ENV = "production";
20
22
  let RELEASE = null;
21
23
  let DEBUG = false;
24
+ let CAPTURE_CONSOLE_ERRORS = true;
22
25
 
23
26
  // Detect environment
24
27
  const isBrowser = typeof window !== "undefined";
@@ -28,6 +31,9 @@ const isNode =
28
31
  // In-memory breadcrumb buffer (shared for all events in this process / page)
29
32
  const MAX_BREADCRUMBS = 100;
30
33
  let breadcrumbs = [];
34
+ let browserInstrumentationInstalled = false;
35
+ let nodeInstrumentationInstalled = false;
36
+ let replayInitPromise = null;
31
37
 
32
38
  export function addBreadcrumb(breadcrumb) {
33
39
  const entry = {
@@ -48,6 +54,10 @@ function getBreadcrumbsSnapshot() {
48
54
  return breadcrumbs.slice();
49
55
  }
50
56
 
57
+ function isWatchForgeInternalMessage(message) {
58
+ return String(message || "").includes("WatchForge SDK");
59
+ }
60
+
51
61
  // Simple browser environment detectors (best-effort, not 100% accurate)
52
62
  function getBrowserContext() {
53
63
  if (!isBrowser) return null;
@@ -150,6 +160,8 @@ function getRuntimeContext() {
150
160
 
151
161
  function setupBrowserInstrumentation() {
152
162
  if (!isBrowser) return;
163
+ if (browserInstrumentationInstalled) return;
164
+ browserInstrumentationInstalled = true;
153
165
 
154
166
  // Global error handlers (already existed but keep here with breadcrumbs)
155
167
  window.onerror = function (msg, url, line, col, error) {
@@ -180,13 +192,42 @@ function setupBrowserInstrumentation() {
180
192
  const original = console[level].bind(console);
181
193
  console[level] = (...args) => {
182
194
  try {
195
+ const message = args.map(String).join(" ");
183
196
  addBreadcrumb({
184
197
  type: "log",
185
198
  level,
186
199
  category: "console",
187
- message: args.map(String).join(" "),
200
+ message,
188
201
  data: {},
189
202
  });
203
+
204
+ if (
205
+ level === "error" &&
206
+ CAPTURE_CONSOLE_ERRORS &&
207
+ !isWatchForgeInternalMessage(message)
208
+ ) {
209
+ const errorArg = args.find((arg) => arg instanceof Error);
210
+ const error =
211
+ errorArg ||
212
+ new Error(message || "console.error");
213
+ setTimeout(() => {
214
+ void captureException(error, {
215
+ tags: {
216
+ handled: true,
217
+ mechanism: "console.error",
218
+ },
219
+ contexts: {
220
+ console: {
221
+ arguments: args.map((arg) =>
222
+ arg instanceof Error
223
+ ? { name: arg.name, message: arg.message, stack: arg.stack }
224
+ : String(arg)
225
+ ),
226
+ },
227
+ },
228
+ });
229
+ }, 0);
230
+ }
190
231
  } catch (_) {
191
232
  // best-effort, never break console
192
233
  }
@@ -389,11 +430,18 @@ export function register({
389
430
  blockClass = "rr-block",
390
431
  ignoreClass = "rr-ignore",
391
432
  maskTextClass = "rr-mask",
433
+ captureConsoleErrors = true,
434
+ projectRoot = null,
392
435
  }) {
393
436
  DSN = dsn;
394
437
  APP_ENV = app_env;
395
438
  RELEASE = release;
396
439
  DEBUG = debug;
440
+ CAPTURE_CONSOLE_ERRORS = Boolean(captureConsoleErrors);
441
+ setProjectRoot(
442
+ projectRoot ||
443
+ (isNode ? process.env.WATCHFORGE_PROJECT_ROOT || process.env.INIT_CWD : null)
444
+ );
397
445
 
398
446
  // Initialize tracing
399
447
  initTracing(dsn, app_env, debug);
@@ -420,7 +468,7 @@ export function register({
420
468
  // Browser: Set up full instrumentation (errors, breadcrumbs, HTTP, navigation)
421
469
  if (isBrowser) {
422
470
  setupBrowserInstrumentation();
423
- initReplay({
471
+ replayInitPromise = initReplay({
424
472
  replaysSessionSampleRate,
425
473
  replaysOnErrorSampleRate,
426
474
  maskAllInputs,
@@ -433,6 +481,9 @@ export function register({
433
481
 
434
482
  // Node.js: Set up process error handlers
435
483
  if (isNode) {
484
+ if (nodeInstrumentationInstalled) return;
485
+ nodeInstrumentationInstalled = true;
486
+
436
487
  process.on("uncaughtException", (error) => {
437
488
  void captureException(error);
438
489
  });
@@ -454,7 +505,10 @@ export async function captureException(error, context = {}) {
454
505
 
455
506
  let stacktrace = buildStacktraceFromError(error);
456
507
  if (stacktrace) {
457
- stacktrace = await enrichStacktraceAsync(stacktrace);
508
+ stacktrace = await enrichStacktraceAsync(
509
+ stacktrace,
510
+ error?.message || String(error || "")
511
+ );
458
512
  }
459
513
 
460
514
  const event = {
@@ -468,6 +522,15 @@ export async function captureException(error, context = {}) {
468
522
  sdk: getSdkMetadata(),
469
523
  };
470
524
 
525
+ if (isBrowser && replayInitPromise) {
526
+ try {
527
+ await replayInitPromise;
528
+ await waitForReplayReady();
529
+ } catch {
530
+ // Replay is best-effort; never block error delivery.
531
+ }
532
+ }
533
+
471
534
  const replay = flushReplayForEvent(DSN, event.event_id);
472
535
  if (replay) {
473
536
  event.replay_id = replay.replay_id;
package/src/contexts.js CHANGED
@@ -7,7 +7,7 @@ const isNode =
7
7
  typeof process !== "undefined" && process.versions && process.versions.node;
8
8
 
9
9
  export const SDK_NAME = "@watchforge/browser";
10
- export const SDK_VERSION = "0.1.10";
10
+ export const SDK_VERSION = "0.1.12";
11
11
 
12
12
  export function getSdkMetadata() {
13
13
  return {
package/src/index.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface WatchForgeRegisterOptions {
9
9
  blockClass?: string;
10
10
  ignoreClass?: string;
11
11
  maskTextClass?: string;
12
+ captureConsoleErrors?: boolean;
13
+ projectRoot?: string | null;
12
14
  }
13
15
 
14
16
  export interface WatchForgeCaptureContext {
@@ -5,6 +5,27 @@ import type {
5
5
 
6
6
  export function register(options: WatchForgeRegisterOptions): void;
7
7
 
8
+ export type NextRequestErrorContext = {
9
+ routerKind: "Pages Router" | "App Router";
10
+ routePath: string;
11
+ routeType: "render" | "route" | "action" | "middleware";
12
+ renderSource?:
13
+ | "react-server-components"
14
+ | "react-server-components-payload"
15
+ | "server-rendering";
16
+ revalidateReason?: "on-demand" | "stale";
17
+ };
18
+
19
+ export function onRequestError(
20
+ error: unknown,
21
+ request: Readonly<{
22
+ path: string;
23
+ method: string;
24
+ headers: Record<string, string | string[] | undefined>;
25
+ }>,
26
+ context: Readonly<NextRequestErrorContext>
27
+ ): void | Promise<void>;
28
+
8
29
  export function captureException(
9
30
  error: unknown,
10
31
  context?: WatchForgeCaptureContext
@@ -1,10 +1,70 @@
1
1
  import {
2
2
  captureException,
3
3
  captureMessage,
4
- register,
4
+ register as registerClient,
5
5
  } from "./client.js";
6
6
 
7
- export { captureException, captureMessage, register };
7
+ let registeredOptions = null;
8
+
9
+ export function register(options) {
10
+ registeredOptions = options;
11
+ registerClient(options);
12
+ }
13
+
14
+ export { captureException, captureMessage };
15
+
16
+ function buildRequestUrl(request) {
17
+ if (!request?.path) return "";
18
+ const path = request.path.startsWith("/") ? request.path : `/${request.path}`;
19
+ return path;
20
+ }
21
+
22
+ export async function onRequestError(error, request, context) {
23
+ if (!registeredOptions) return;
24
+
25
+ await captureException(error, {
26
+ request: {
27
+ url: buildRequestUrl(request),
28
+ method: request?.method || "GET",
29
+ headers: sanitizeRequestHeaders(request?.headers),
30
+ },
31
+ tags: {
32
+ framework: "nextjs",
33
+ runtime: "server",
34
+ router: context?.routerKind || "unknown",
35
+ route_type: context?.routeType || "unknown",
36
+ },
37
+ contexts: {
38
+ nextjs: {
39
+ on_request_error: true,
40
+ router_kind: context?.routerKind || null,
41
+ route_path: context?.routePath || null,
42
+ route_type: context?.routeType || null,
43
+ render_source: context?.renderSource || null,
44
+ },
45
+ },
46
+ });
47
+ }
48
+
49
+ function sanitizeRequestHeaders(headers) {
50
+ if (!headers) return {};
51
+
52
+ const sensitive = new Set([
53
+ "authorization",
54
+ "cookie",
55
+ "set-cookie",
56
+ "x-api-key",
57
+ "x-csrftoken",
58
+ ]);
59
+ const entries =
60
+ typeof headers.entries === "function"
61
+ ? Array.from(headers.entries())
62
+ : Object.entries(headers);
63
+
64
+ return Object.fromEntries(
65
+ entries.filter(([key]) => !sensitive.has(String(key).toLowerCase()))
66
+ );
67
+ }
8
68
 
9
69
  function sanitizeHeaders(headers) {
10
70
  if (!headers || typeof headers.entries !== "function") return {};
package/src/replay.js CHANGED
@@ -120,6 +120,27 @@ export async function initReplay(config = {}) {
120
120
  }
121
121
  }
122
122
 
123
+ export function waitForReplayReady(timeoutMs = 300) {
124
+ if (!isBrowser || !replayId || !sessionId) return Promise.resolve(false);
125
+ if (events.length > 0) return Promise.resolve(true);
126
+
127
+ return new Promise((resolve) => {
128
+ const started = Date.now();
129
+ const check = () => {
130
+ if (events.length > 0) {
131
+ resolve(true);
132
+ return;
133
+ }
134
+ if (Date.now() - started >= timeoutMs) {
135
+ resolve(false);
136
+ return;
137
+ }
138
+ setTimeout(check, 25);
139
+ };
140
+ check();
141
+ });
142
+ }
143
+
123
144
  export function getReplayContext() {
124
145
  if (!isBrowser || !replayId || !sessionId) return null;
125
146
  return {
@@ -151,7 +172,7 @@ export function flushReplayForEvent(dsn, eventId) {
151
172
  events,
152
173
  sdk: {
153
174
  name: "@watchforge/browser",
154
- version: "0.1.10",
175
+ version: "0.1.12",
155
176
  },
156
177
  };
157
178
 
package/src/stacktrace.js CHANGED
@@ -60,6 +60,7 @@ export function buildStacktraceFromError(error) {
60
60
  return null;
61
61
  }
62
62
 
63
+ const errorMessage = error?.message || "";
63
64
  const stackLines = error.stack.split("\n");
64
65
  const frames = [];
65
66
 
@@ -88,6 +89,7 @@ export function buildStacktraceFromError(error) {
88
89
  post_context: [],
89
90
  vars: {},
90
91
  in_app: inApp,
92
+ error_message: errorMessage,
91
93
  });
92
94
  }
93
95
 
@@ -114,6 +116,45 @@ function applySourceContext(frame, sourceLines, lineno) {
114
116
  .map((l) => l.replace(/\r$/, ""));
115
117
  }
116
118
 
119
+ function getUndefinedIdentifier(message) {
120
+ if (!message || typeof message !== "string") return null;
121
+ const match = message.match(/^([A-Za-z_$][\w$]*) is not defined$/);
122
+ return match?.[1] || null;
123
+ }
124
+
125
+ function inferLineFromErrorMessage(frame, sourceLines) {
126
+ const identifier = getUndefinedIdentifier(frame.error_message);
127
+ if (!identifier || !sourceLines?.length) return null;
128
+
129
+ const tokenPattern = new RegExp(`\\b${identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
130
+ const functionName = frame.function && frame.function !== "<anonymous>"
131
+ ? String(frame.function)
132
+ : null;
133
+
134
+ const candidates = [];
135
+ for (let i = 0; i < sourceLines.length; i++) {
136
+ if (!tokenPattern.test(sourceLines[i])) continue;
137
+ candidates.push(i);
138
+ }
139
+
140
+ if (!candidates.length) return null;
141
+
142
+ if (functionName) {
143
+ const functionPattern = new RegExp(`\\b${functionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
144
+ let nearestFunctionLine = -1;
145
+ for (let i = 0; i < sourceLines.length; i++) {
146
+ if (functionPattern.test(sourceLines[i])) {
147
+ nearestFunctionLine = i;
148
+ }
149
+ if (candidates.includes(i) && nearestFunctionLine !== -1) {
150
+ return i + 1;
151
+ }
152
+ }
153
+ }
154
+
155
+ return candidates[0] + 1;
156
+ }
157
+
117
158
  function normalizeSourcePath(source) {
118
159
  if (!source) return "";
119
160
 
@@ -167,6 +208,114 @@ function getNodeModules() {
167
208
  return nodeModulesPromise;
168
209
  }
169
210
 
211
+ let cachedProjectRoots = null;
212
+ let configuredProjectRoot = null;
213
+
214
+ export function setProjectRoot(projectRoot) {
215
+ configuredProjectRoot = projectRoot || null;
216
+ cachedProjectRoots = null;
217
+ }
218
+
219
+ function packageJsonHasNext(packageJson) {
220
+ return Boolean(packageJson.dependencies?.next || packageJson.devDependencies?.next);
221
+ }
222
+
223
+ async function findProjectRoots() {
224
+ if (cachedProjectRoots) return cachedProjectRoots;
225
+
226
+ const mods = await getNodeModules();
227
+ if (!mods) {
228
+ cachedProjectRoots = [process.cwd()];
229
+ return cachedProjectRoots;
230
+ }
231
+
232
+ const { fs, path } = mods;
233
+ const roots = new Set();
234
+ const seeds = [
235
+ configuredProjectRoot,
236
+ process.env.WATCHFORGE_PROJECT_ROOT,
237
+ process.env.INIT_CWD,
238
+ process.env.PWD,
239
+ process.cwd(),
240
+ ].filter(Boolean);
241
+
242
+ for (const seed of seeds) {
243
+ roots.add(seed);
244
+
245
+ let dir = seed;
246
+ for (let depth = 0; depth < 10; depth++) {
247
+ const packageJsonPath = path.join(dir, "package.json");
248
+ if (fs.existsSync(packageJsonPath)) {
249
+ try {
250
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
251
+ if (packageJsonHasNext(packageJson)) {
252
+ roots.add(dir);
253
+ }
254
+ } catch {
255
+ // ignore invalid package.json
256
+ }
257
+ }
258
+
259
+ const parent = path.dirname(dir);
260
+ if (parent === dir) break;
261
+ dir = parent;
262
+ }
263
+ }
264
+
265
+ cachedProjectRoots = [...roots];
266
+ return cachedProjectRoots;
267
+ }
268
+
269
+ function readFileLines(fs, filePath) {
270
+ const stat = fs.statSync(filePath);
271
+ if (stat.size > MAX_SOURCE_FILE_BYTES) return null;
272
+ return fs.readFileSync(filePath, "utf8").split("\n");
273
+ }
274
+
275
+ function findFileUnderRoot(fs, path, root, normalizedPath, basename, maxDepth = 5) {
276
+ const suffix = normalizedPath.replace(/^\/+/, "");
277
+ const directCandidate = path.join(root, suffix);
278
+ if (fs.existsSync(directCandidate)) {
279
+ return directCandidate;
280
+ }
281
+
282
+ const stack = [{ dir: root, depth: 0 }];
283
+ let scannedFiles = 0;
284
+
285
+ while (stack.length > 0 && scannedFiles < 500) {
286
+ const { dir, depth } = stack.pop();
287
+ if (depth > maxDepth) continue;
288
+
289
+ let entries = [];
290
+ try {
291
+ entries = fs.readdirSync(dir, { withFileTypes: true });
292
+ } catch {
293
+ continue;
294
+ }
295
+
296
+ for (const entry of entries) {
297
+ if (entry.name === "node_modules" || entry.name === ".next" || entry.name === ".git") {
298
+ continue;
299
+ }
300
+
301
+ const entryPath = path.join(dir, entry.name);
302
+ if (entry.isFile()) {
303
+ scannedFiles += 1;
304
+ if (entry.name === basename) {
305
+ const normalizedEntry = entryPath.replace(/\\/g, "/");
306
+ if (normalizedEntry.endsWith(suffix)) {
307
+ return entryPath;
308
+ }
309
+ }
310
+ } else if (entry.isDirectory()) {
311
+ stack.push({ dir: entryPath, depth: depth + 1 });
312
+ }
313
+ }
314
+ }
315
+
316
+ return null;
317
+ }
318
+
170
319
  async function readNodeSourceLines(absPath) {
171
320
  if (!isNode || !absPath) return null;
172
321
 
@@ -185,26 +334,87 @@ async function readNodeSourceLines(absPath) {
185
334
  filePath = fileURLToPath(filePath);
186
335
  }
187
336
 
188
- if (!path.isAbsolute(filePath)) {
189
- filePath = path.resolve(process.cwd(), filePath);
337
+ const normalizedPath = normalizeSourcePath(filePath);
338
+ const roots = await findProjectRoots();
339
+ const candidates = new Set();
340
+
341
+ if (path.isAbsolute(filePath)) {
342
+ candidates.add(filePath);
343
+ }
344
+
345
+ for (const root of roots) {
346
+ candidates.add(path.resolve(root, filePath));
347
+ candidates.add(path.resolve(root, normalizedPath));
348
+
349
+ const srcMatch = normalizedPath.match(/(?:^|\/)?(src\/.+)$/);
350
+ if (srcMatch) {
351
+ candidates.add(path.resolve(root, srcMatch[1]));
352
+ }
353
+ }
354
+
355
+ for (const candidate of candidates) {
356
+ if (!candidate || !fs.existsSync(candidate)) continue;
357
+ const lines = readFileLines(fs, candidate);
358
+ if (lines) return lines;
190
359
  }
191
360
 
192
- if (!fs.existsSync(filePath)) return null;
361
+ const basename = path.basename(normalizedPath);
362
+ for (const root of roots) {
363
+ const packageJsonPath = path.join(root, "package.json");
364
+ if (!fs.existsSync(packageJsonPath)) continue;
365
+
366
+ let isNextProject = root === process.env.INIT_CWD || root === process.env.PWD;
367
+ if (!isNextProject) {
368
+ try {
369
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
370
+ isNextProject = packageJsonHasNext(packageJson);
371
+ } catch {
372
+ isNextProject = false;
373
+ }
374
+ }
193
375
 
194
- const stat = fs.statSync(filePath);
195
- if (stat.size > MAX_SOURCE_FILE_BYTES) return null;
376
+ if (!isNextProject) continue;
377
+
378
+ const discovered = findFileUnderRoot(fs, path, root, normalizedPath, basename);
379
+ if (discovered && fs.existsSync(discovered)) {
380
+ const lines = readFileLines(fs, discovered);
381
+ if (lines) return lines;
382
+ }
383
+ }
196
384
 
197
- const content = fs.readFileSync(filePath, "utf8");
198
- return content.split("\n");
385
+ return null;
199
386
  } catch {
200
387
  return null;
201
388
  }
202
389
  }
203
390
 
391
+ function resolveFrameLine(frame, lines) {
392
+ let lineno = frame.lineno;
393
+ if (!lines?.length || !lineno) return lineno;
394
+
395
+ if (lineno > lines.length) {
396
+ const inferredLine = inferLineFromErrorMessage(frame, lines);
397
+ if (inferredLine) {
398
+ lineno = inferredLine;
399
+ frame.lineno = inferredLine;
400
+ const identifier = getUndefinedIdentifier(frame.error_message);
401
+ if (identifier) {
402
+ const col = lines[inferredLine - 1].indexOf(identifier);
403
+ if (col >= 0) frame.colno = col + 1;
404
+ }
405
+ }
406
+ }
407
+
408
+ return lineno;
409
+ }
410
+
204
411
  async function enrichFrameWithNodeSource(frame) {
205
412
  if (!frame.in_app || !frame.lineno) return;
206
413
  const lines = await readNodeSourceLines(frame.raw_abs_path || frame.abs_path);
207
- if (lines) applySourceContext(frame, lines, frame.lineno);
414
+ if (!lines) return;
415
+
416
+ const lineno = resolveFrameLine(frame, lines);
417
+ applySourceContext(frame, lines, lineno);
208
418
  }
209
419
 
210
420
  function getBrowserSourceFetchCandidates(absPath, rawAbsPath) {
@@ -475,13 +685,21 @@ async function enrichFrameWithBrowserSource(frame) {
475
685
  await enrichFrameWithSourceMap(frame);
476
686
  }
477
687
 
478
- export async function enrichStacktraceAsync(stacktrace) {
688
+ export async function enrichStacktraceAsync(stacktrace, errorMessage = null) {
479
689
  if (!stacktrace?.frames?.length) return stacktrace;
480
690
 
481
691
  const inAppFrames = stacktrace.frames.filter((f) => f.in_app);
482
692
  // Enrich the error frame first (last in-app frame), then nearby frames.
483
693
  const targets = [...inAppFrames.slice(-5)].reverse();
484
694
 
695
+ if (errorMessage) {
696
+ for (const frame of targets) {
697
+ if (!frame.error_message) {
698
+ frame.error_message = errorMessage;
699
+ }
700
+ }
701
+ }
702
+
485
703
  if (isNode) {
486
704
  await Promise.all(targets.map((frame) => enrichFrameWithNodeSource(frame)));
487
705
  } else if (isBrowser) {
package/src/tracing.js CHANGED
@@ -118,7 +118,7 @@ class Transaction {
118
118
  request: this.request,
119
119
  platform: typeof window !== "undefined" ? "javascript" : "node",
120
120
  sdk_name: "watchforge-javascript",
121
- sdk_version: "0.1.0",
121
+ sdk_version: "0.1.12",
122
122
  };
123
123
 
124
124
  if (DEBUG) {