@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 +45 -0
- package/package.json +5 -1
- package/src/contexts.js +6 -2
- package/src/next-config.d.ts +6 -0
- package/src/next-config.js +135 -0
- package/src/replay.js +39 -3
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.
|
|
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.
|
|
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 =
|
|
83
|
+
nodeOsPromise = importNodeBuiltin("os").catch(() => null);
|
|
80
84
|
}
|
|
81
85
|
return nodeOsPromise;
|
|
82
86
|
}
|
|
@@ -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
|
|
41
|
-
|
|
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.
|
|
154
|
+
version: "0.1.10",
|
|
119
155
|
},
|
|
120
156
|
};
|
|
121
157
|
|