@watchforge/browser 0.1.7 → 0.1.10

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
@@ -136,6 +136,16 @@ function findPagesApp(cwd) {
136
136
  return candidates.find(fileExists) || null;
137
137
  }
138
138
 
139
+ function findNextConfig(cwd) {
140
+ const candidates = [
141
+ "next.config.ts",
142
+ "next.config.mts",
143
+ "next.config.js",
144
+ "next.config.mjs",
145
+ ];
146
+ return candidates.map((candidate) => path.join(cwd, candidate)).find(fileExists) || null;
147
+ }
148
+
139
149
  function writeIfChanged(filePath, content) {
140
150
  if (fileExists(filePath) && fs.readFileSync(filePath, "utf8") === content) {
141
151
  return false;
@@ -361,6 +371,39 @@ export async function register() {
361
371
  log(`wrote ${path.relative(cwd, instrumentationPath)}`);
362
372
  }
363
373
 
374
+ function patchNextConfig(cwd) {
375
+ const configPath = findNextConfig(cwd);
376
+ if (!configPath) {
377
+ log("could not find next.config.* to patch for compiler error capture");
378
+ log('wrap your Next config with withWatchForgeConfig(config, watchforgeConfig) from "@watchforge/browser/next/config"');
379
+ return;
380
+ }
381
+
382
+ let content = fs.readFileSync(configPath, "utf8");
383
+ if (content.includes("withWatchForgeConfig")) {
384
+ log(`${path.relative(cwd, configPath)} already contains WatchForge compiler setup`);
385
+ return;
386
+ }
387
+
388
+ const configImport = toImportPath(path.dirname(configPath), getConfigPath(cwd));
389
+ content = `import { withWatchForgeConfig } from "@watchforge/browser/next/config";\nimport { watchforgeConfig } from "${configImport}";\n${content}`;
390
+
391
+ const exportDefaultMatch = content.match(/export\s+default\s+([^;]+);/);
392
+ if (!exportDefaultMatch) {
393
+ log(`skipped ${path.relative(cwd, configPath)} because export default could not be patched automatically`);
394
+ log('wrap your exported config with withWatchForgeConfig(config, watchforgeConfig)');
395
+ return;
396
+ }
397
+
398
+ content = content.replace(
399
+ exportDefaultMatch[0],
400
+ `export default withWatchForgeConfig(${exportDefaultMatch[1]}, watchforgeConfig);`
401
+ );
402
+
403
+ fs.writeFileSync(configPath, content);
404
+ log(`patched ${path.relative(cwd, configPath)} for compiler error capture`);
405
+ }
406
+
364
407
  function patchPagesRouter(cwd, appPath) {
365
408
  const pagesDir = path.dirname(appPath);
366
409
  const configImport = toImportPath(pagesDir, getConfigPath(cwd));
@@ -522,6 +565,7 @@ function initNextjs(args) {
522
565
 
523
566
  const layoutPath = findNextLayout(cwd);
524
567
  if (layoutPath) {
568
+ patchNextConfig(cwd);
525
569
  patchAppRouter(cwd, layoutPath);
526
570
  createNextGlobalError(cwd, layoutPath);
527
571
  createNextInstrumentation(cwd, layoutPath);
@@ -530,6 +574,7 @@ function initNextjs(args) {
530
574
 
531
575
  const appPath = findPagesApp(cwd);
532
576
  if (appPath) {
577
+ patchNextConfig(cwd);
533
578
  patchPagesRouter(cwd, appPath);
534
579
  return;
535
580
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@watchforge/browser",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
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",
@@ -33,6 +33,10 @@
33
33
  "types": "./src/next-server.d.ts",
34
34
  "default": "./src/next-server.js"
35
35
  },
36
+ "./next/config": {
37
+ "types": "./src/next-config.d.ts",
38
+ "default": "./src/next-config.js"
39
+ },
36
40
  "./express": {
37
41
  "types": "./src/express.d.ts",
38
42
  "default": "./src/express.js"
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.1";
10
+ export const SDK_VERSION = "0.1.10";
11
11
 
12
12
  export function getSdkMetadata() {
13
13
  return {
@@ -73,10 +73,14 @@ export function getPerformanceContext() {
73
73
 
74
74
  let nodeOsPromise = null;
75
75
 
76
+ // Hide from bundlers (Next.js/webpack) so client builds don't resolve Node built-ins.
77
+ const importNodeBuiltin = (specifier) =>
78
+ new Function("specifier", "return import(specifier)")(specifier);
79
+
76
80
  async function getNodeOs() {
77
81
  if (!isNode) return null;
78
82
  if (!nodeOsPromise) {
79
- nodeOsPromise = import("os").catch(() => null);
83
+ nodeOsPromise = importNodeBuiltin("os").catch(() => null);
80
84
  }
81
85
  return nodeOsPromise;
82
86
  }
@@ -0,0 +1,6 @@
1
+ import type { WatchForgeRegisterOptions } from "./index";
2
+
3
+ export function withWatchForgeConfig<T extends Record<string, unknown>>(
4
+ nextConfig?: T,
5
+ watchforgeOptions?: WatchForgeRegisterOptions
6
+ ): T;
@@ -0,0 +1,135 @@
1
+ import { captureException, register } from "./client.js";
2
+
3
+ const PLUGIN_NAME = "WatchForgeNextCompilerPlugin";
4
+ const sentCompilerErrors = new Set();
5
+ let registeredDsn = null;
6
+
7
+ function getLoc(rawError) {
8
+ const loc = rawError?.loc || rawError?.module?.loc;
9
+
10
+ if (typeof loc === "string") {
11
+ const match = loc.match(/(\d+)(?::(\d+))?/);
12
+ if (match) {
13
+ return {
14
+ line: Number(match[1]),
15
+ column: Number(match[2] || 1),
16
+ };
17
+ }
18
+ }
19
+
20
+ if (loc && typeof loc === "object") {
21
+ const start = loc.start || loc;
22
+ return {
23
+ line: Number(start.line || start.lineNumber || 1),
24
+ column: Number(start.column || start.col || 1),
25
+ };
26
+ }
27
+
28
+ return { line: 1, column: 1 };
29
+ }
30
+
31
+ function getModulePath(rawError) {
32
+ return (
33
+ rawError?.module?.resource ||
34
+ rawError?.module?.resourceResolveData?.path ||
35
+ rawError?.file ||
36
+ rawError?.moduleName ||
37
+ null
38
+ );
39
+ }
40
+
41
+ function createCompilerException(rawError) {
42
+ const message =
43
+ rawError?.message ||
44
+ rawError?.details ||
45
+ String(rawError || "Next.js compiler error");
46
+ const error = new Error(message);
47
+ error.name = rawError?.name || "NextWebpackCompilationError";
48
+
49
+ const filePath = getModulePath(rawError);
50
+ if (filePath) {
51
+ const { line, column } = getLoc(rawError);
52
+ error.stack = `${error.name}: ${message}\n at Next.js compilation (${filePath}:${line}:${column})`;
53
+ if (rawError?.stack) {
54
+ error.stack += `\n${String(rawError.stack).split("\n").slice(1).join("\n")}`;
55
+ }
56
+ } else if (rawError?.stack) {
57
+ error.stack = rawError.stack;
58
+ }
59
+
60
+ return error;
61
+ }
62
+
63
+ function getCompilerErrorFingerprint(rawError, compilerName, hash) {
64
+ const filePath = getModulePath(rawError) || "";
65
+ const loc = getLoc(rawError);
66
+ const message = rawError?.message || String(rawError || "");
67
+ return [hash || "", compilerName || "", filePath, loc.line, loc.column, message].join("|");
68
+ }
69
+
70
+ function createCompilerPlugin() {
71
+ return {
72
+ name: PLUGIN_NAME,
73
+ apply(compiler) {
74
+ compiler.hooks.done.tapPromise(PLUGIN_NAME, async (stats) => {
75
+ if (!stats?.hasErrors?.()) return;
76
+
77
+ const errors = stats.compilation?.errors || [];
78
+ const compilerName = compiler.name || stats.compilation?.name || null;
79
+
80
+ for (const rawError of errors) {
81
+ const fingerprint = getCompilerErrorFingerprint(rawError, compilerName, stats.hash);
82
+ if (sentCompilerErrors.has(fingerprint)) continue;
83
+ sentCompilerErrors.add(fingerprint);
84
+
85
+ await captureException(createCompilerException(rawError), {
86
+ tags: {
87
+ framework: "nextjs",
88
+ runtime: "compiler",
89
+ compiler: compilerName,
90
+ },
91
+ contexts: {
92
+ nextjs: {
93
+ compiler_error: true,
94
+ module: getModulePath(rawError),
95
+ loc: rawError?.loc || rawError?.module?.loc || null,
96
+ },
97
+ },
98
+ extra: {
99
+ details: rawError?.details || null,
100
+ module_identifier: rawError?.module?.identifier?.() || null,
101
+ module_name: rawError?.module?.readableIdentifier?.({}) || rawError?.moduleName || null,
102
+ },
103
+ });
104
+ }
105
+ });
106
+ },
107
+ };
108
+ }
109
+
110
+ function ensureRegistered(options) {
111
+ if (!options?.dsn || registeredDsn === options.dsn) return;
112
+ registeredDsn = options.dsn;
113
+ register(options);
114
+ }
115
+
116
+ export function withWatchForgeConfig(nextConfig = {}, watchforgeOptions = {}) {
117
+ ensureRegistered(watchforgeOptions);
118
+
119
+ const userWebpack = nextConfig.webpack;
120
+
121
+ return {
122
+ ...nextConfig,
123
+ webpack(config, options) {
124
+ const finalConfig = userWebpack ? userWebpack(config, options) || config : config;
125
+ finalConfig.plugins = finalConfig.plugins || [];
126
+
127
+ const hasPlugin = finalConfig.plugins.some((plugin) => plugin?.name === PLUGIN_NAME);
128
+ if (!hasPlugin) {
129
+ finalConfig.plugins.push(createCompilerPlugin());
130
+ }
131
+
132
+ return finalConfig;
133
+ },
134
+ };
135
+ }
package/src/replay.js CHANGED
@@ -3,6 +3,8 @@ import { sendReplay } from "./transport.js";
3
3
  const isBrowser = typeof window !== "undefined";
4
4
  const MAX_BUFFER_MS = 60 * 1000;
5
5
  const MAX_EVENTS = 500;
6
+ const RRWEB_FULL_SNAPSHOT = 2;
7
+ const RRWEB_META = 4;
6
8
 
7
9
  let options = {
8
10
  replaysSessionSampleRate: 0,
@@ -37,9 +39,43 @@ function shouldSample(rate) {
37
39
 
38
40
  function trimBuffer(now = Date.now()) {
39
41
  events = events.filter((event) => now - event.timestamp <= MAX_BUFFER_MS);
40
- if (events.length > MAX_EVENTS) {
41
- events = events.slice(-MAX_EVENTS);
42
+ if (events.length <= MAX_EVENTS) return;
43
+
44
+ const tail = events.slice(-MAX_EVENTS);
45
+ if (tail.some((event) => event.type === RRWEB_FULL_SNAPSHOT)) {
46
+ events = tail;
47
+ return;
48
+ }
49
+
50
+ const fullSnapshotIndex = findLastEventIndex(events, RRWEB_FULL_SNAPSHOT);
51
+ if (fullSnapshotIndex === -1) {
52
+ events = tail;
53
+ return;
54
+ }
55
+
56
+ const metaIndex = findLastEventIndex(
57
+ events.slice(0, fullSnapshotIndex),
58
+ RRWEB_META
59
+ );
60
+ const anchorEvents = [
61
+ ...(metaIndex >= 0 ? [events[metaIndex]] : []),
62
+ events[fullSnapshotIndex],
63
+ ];
64
+ const remainingSlots = Math.max(MAX_EVENTS - anchorEvents.length, 0);
65
+ const recentEvents = events
66
+ .slice(fullSnapshotIndex + 1)
67
+ .slice(-remainingSlots);
68
+
69
+ events = [...anchorEvents, ...recentEvents].sort(
70
+ (a, b) => (a.timestamp || 0) - (b.timestamp || 0)
71
+ );
72
+ }
73
+
74
+ function findLastEventIndex(list, type) {
75
+ for (let i = list.length - 1; i >= 0; i--) {
76
+ if (list[i]?.type === type) return i;
42
77
  }
78
+ return -1;
43
79
  }
44
80
 
45
81
  export async function initReplay(config = {}) {
@@ -115,7 +151,7 @@ export function flushReplayForEvent(dsn, eventId) {
115
151
  events,
116
152
  sdk: {
117
153
  name: "@watchforge/browser",
118
- version: "0.1.3",
154
+ version: "0.1.10",
119
155
  },
120
156
  };
121
157