@alwaysmeticulous/debug-workspace 2.261.0
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/LICENSE +15 -0
- package/dist/debug-constants.d.ts +3 -0
- package/dist/debug-constants.js +10 -0
- package/dist/debug-constants.js.map +1 -0
- package/dist/debug.types.d.ts +21 -0
- package/dist/debug.types.js +3 -0
- package/dist/debug.types.js.map +1 -0
- package/dist/download-debug-data.d.ts +10 -0
- package/dist/download-debug-data.js +141 -0
- package/dist/download-debug-data.js.map +1 -0
- package/dist/generate-debug-workspace.d.ts +31 -0
- package/dist/generate-debug-workspace.js +1302 -0
- package/dist/generate-debug-workspace.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline.d.ts +21 -0
- package/dist/pipeline.js +63 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/resolve-debug-context.d.ts +12 -0
- package/dist/resolve-debug-context.js +187 -0
- package/dist/resolve-debug-context.js.map +1 -0
- package/dist/templates/CLAUDE.md +206 -0
- package/dist/templates/agents/planner.md +65 -0
- package/dist/templates/agents/summarizer.md +75 -0
- package/dist/templates/hooks/check-file-size.sh +36 -0
- package/dist/templates/hooks/load-context.sh +20 -0
- package/dist/templates/rules/feedback.md +37 -0
- package/dist/templates/settings.json +82 -0
- package/dist/templates/skills/debugging-diffs/SKILL.md +57 -0
- package/dist/templates/skills/debugging-flakes/SKILL.md +52 -0
- package/dist/templates/skills/debugging-network/SKILL.md +45 -0
- package/dist/templates/skills/debugging-sessions/SKILL.md +47 -0
- package/dist/templates/skills/debugging-timelines/SKILL.md +51 -0
- package/dist/templates/skills/pr-analysis/SKILL.md +20 -0
- package/dist/templates/templates/CLAUDE.md +206 -0
- package/dist/templates/templates/agents/planner.md +65 -0
- package/dist/templates/templates/agents/summarizer.md +75 -0
- package/dist/templates/templates/hooks/check-file-size.sh +36 -0
- package/dist/templates/templates/hooks/load-context.sh +20 -0
- package/dist/templates/templates/rules/feedback.md +37 -0
- package/dist/templates/templates/settings.json +82 -0
- package/dist/templates/templates/skills/debugging-diffs/SKILL.md +57 -0
- package/dist/templates/templates/skills/debugging-flakes/SKILL.md +52 -0
- package/dist/templates/templates/skills/debugging-network/SKILL.md +45 -0
- package/dist/templates/templates/skills/debugging-sessions/SKILL.md +47 -0
- package/dist/templates/templates/skills/debugging-timelines/SKILL.md +51 -0
- package/dist/templates/templates/skills/pr-analysis/SKILL.md +20 -0
- package/package.json +49 -0
|
@@ -0,0 +1,1302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateDebugWorkspace = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const common_1 = require("@alwaysmeticulous/common");
|
|
12
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
13
|
+
const debug_constants_1 = require("./debug-constants");
|
|
14
|
+
const TEMPLATES_DIR = (0, path_1.join)(__dirname, "templates");
|
|
15
|
+
const generateDebugWorkspace = (options) => {
|
|
16
|
+
var _a;
|
|
17
|
+
const { debugContext, workspaceDir } = options;
|
|
18
|
+
const claudeDir = (0, path_1.join)(workspaceDir, ".claude");
|
|
19
|
+
(0, fs_1.mkdirSync)(claudeDir, { recursive: true });
|
|
20
|
+
copyClaudeMd(workspaceDir, options.additionalTemplatesDir);
|
|
21
|
+
generateFilteredLogs(workspaceDir);
|
|
22
|
+
generateLogDiffs(debugContext, workspaceDir);
|
|
23
|
+
generateDiffSummaries(workspaceDir);
|
|
24
|
+
generatePrDiff(debugContext, workspaceDir);
|
|
25
|
+
generateParamsDiffs(debugContext, workspaceDir);
|
|
26
|
+
generateAssetsDiff(debugContext, workspaceDir);
|
|
27
|
+
generateTimelineSummaries(workspaceDir);
|
|
28
|
+
generateSessionSummaries(debugContext, workspaceDir);
|
|
29
|
+
prettifySnapshotAssets(workspaceDir);
|
|
30
|
+
const screenshotMap = buildScreenshotMap(debugContext, workspaceDir);
|
|
31
|
+
generateScreenshotContext(debugContext, workspaceDir, screenshotMap);
|
|
32
|
+
const replayComparison = buildReplayComparison(debugContext, workspaceDir);
|
|
33
|
+
const fileMetadata = collectFileMetadata(debugContext, workspaceDir);
|
|
34
|
+
const writeCtx = (_a = options.writeContextJson) !== null && _a !== void 0 ? _a : defaultWriteContextJson;
|
|
35
|
+
writeCtx(debugContext, workspaceDir, fileMetadata, options.projectRepoDir, screenshotMap, replayComparison);
|
|
36
|
+
copyClaudeSubdir(workspaceDir, "rules", options.additionalTemplatesDir);
|
|
37
|
+
copyClaudeSubdir(workspaceDir, "hooks", options.additionalTemplatesDir);
|
|
38
|
+
copyClaudeSubdir(workspaceDir, "agents", options.additionalTemplatesDir);
|
|
39
|
+
copySkills(workspaceDir, options.additionalTemplatesDir);
|
|
40
|
+
const settingsSrc = resolveTemplateFile("settings.json", options.additionalTemplatesDir);
|
|
41
|
+
if (settingsSrc) {
|
|
42
|
+
(0, fs_1.copyFileSync)(settingsSrc, (0, path_1.join)(claudeDir, "settings.json"));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
exports.generateDebugWorkspace = generateDebugWorkspace;
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Template copying (with overlay support)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
const resolveTemplateFile = (relativePath, additionalTemplatesDir) => {
|
|
50
|
+
if (additionalTemplatesDir) {
|
|
51
|
+
const overlayPath = (0, path_1.join)(additionalTemplatesDir, relativePath);
|
|
52
|
+
if ((0, fs_1.existsSync)(overlayPath)) {
|
|
53
|
+
return overlayPath;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const basePath = (0, path_1.join)(TEMPLATES_DIR, relativePath);
|
|
57
|
+
if ((0, fs_1.existsSync)(basePath)) {
|
|
58
|
+
return basePath;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
};
|
|
62
|
+
const copyClaudeMd = (workspaceDir, additionalTemplatesDir) => {
|
|
63
|
+
const src = resolveTemplateFile("CLAUDE.md", additionalTemplatesDir);
|
|
64
|
+
if (src) {
|
|
65
|
+
(0, fs_1.copyFileSync)(src, (0, path_1.join)(workspaceDir, ".claude", "CLAUDE.md"));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const copyClaudeSubdir = (workspaceDir, subdir, additionalTemplatesDir) => {
|
|
69
|
+
const destDir = (0, path_1.join)(workspaceDir, ".claude", subdir);
|
|
70
|
+
let copied = false;
|
|
71
|
+
const baseSrcDir = (0, path_1.join)(TEMPLATES_DIR, subdir);
|
|
72
|
+
if ((0, fs_1.existsSync)(baseSrcDir)) {
|
|
73
|
+
const entries = (0, fs_1.readdirSync)(baseSrcDir).filter((f) => !(0, fs_1.statSync)((0, path_1.join)(baseSrcDir, f)).isDirectory());
|
|
74
|
+
if (entries.length > 0) {
|
|
75
|
+
(0, fs_1.mkdirSync)(destDir, { recursive: true });
|
|
76
|
+
for (const filename of entries) {
|
|
77
|
+
(0, fs_1.copyFileSync)((0, path_1.join)(baseSrcDir, filename), (0, path_1.join)(destDir, filename));
|
|
78
|
+
}
|
|
79
|
+
copied = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (additionalTemplatesDir) {
|
|
83
|
+
const overlaySrcDir = (0, path_1.join)(additionalTemplatesDir, subdir);
|
|
84
|
+
if ((0, fs_1.existsSync)(overlaySrcDir)) {
|
|
85
|
+
const entries = (0, fs_1.readdirSync)(overlaySrcDir).filter((f) => !(0, fs_1.statSync)((0, path_1.join)(overlaySrcDir, f)).isDirectory());
|
|
86
|
+
if (entries.length > 0) {
|
|
87
|
+
if (!copied) {
|
|
88
|
+
(0, fs_1.mkdirSync)(destDir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
for (const filename of entries) {
|
|
91
|
+
(0, fs_1.copyFileSync)((0, path_1.join)(overlaySrcDir, filename), (0, path_1.join)(destDir, filename));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const copySkills = (workspaceDir, additionalTemplatesDir) => {
|
|
98
|
+
const skillDirNames = new Set();
|
|
99
|
+
const baseSrcDir = (0, path_1.join)(TEMPLATES_DIR, "skills");
|
|
100
|
+
if ((0, fs_1.existsSync)(baseSrcDir)) {
|
|
101
|
+
for (const entry of (0, fs_1.readdirSync)(baseSrcDir)) {
|
|
102
|
+
if ((0, fs_1.statSync)((0, path_1.join)(baseSrcDir, entry)).isDirectory()) {
|
|
103
|
+
skillDirNames.add(entry);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (additionalTemplatesDir) {
|
|
108
|
+
const overlaySrcDir = (0, path_1.join)(additionalTemplatesDir, "skills");
|
|
109
|
+
if ((0, fs_1.existsSync)(overlaySrcDir)) {
|
|
110
|
+
for (const entry of (0, fs_1.readdirSync)(overlaySrcDir)) {
|
|
111
|
+
if ((0, fs_1.statSync)((0, path_1.join)(overlaySrcDir, entry)).isDirectory()) {
|
|
112
|
+
skillDirNames.add(entry);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const skillName of skillDirNames) {
|
|
118
|
+
copyClaudeSubdir(workspaceDir, (0, path_1.join)("skills", skillName), additionalTemplatesDir);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Log filtering and diffs
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
const generateFilteredLogs = (workspaceDir) => {
|
|
125
|
+
const replaySubDirs = ["head", "base", "other"];
|
|
126
|
+
let count = 0;
|
|
127
|
+
for (const subDir of replaySubDirs) {
|
|
128
|
+
const subDirPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", subDir);
|
|
129
|
+
if (!(0, fs_1.existsSync)(subDirPath)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
for (const replayId of (0, fs_1.readdirSync)(subDirPath)) {
|
|
133
|
+
const logPath = (0, path_1.join)(subDirPath, replayId, "logs.deterministic.txt");
|
|
134
|
+
if (!(0, fs_1.existsSync)(logPath)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const raw = (0, fs_1.readFileSync)(logPath, "utf8");
|
|
138
|
+
const filtered = filterLogLines(raw);
|
|
139
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(subDirPath, replayId, "logs.deterministic.filtered.txt"), filtered);
|
|
140
|
+
count++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (count > 0) {
|
|
144
|
+
console.log(chalk_1.default.green(` Generated ${count} filtered log file(s)`));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const LOG_NOISE_PATTERNS = [
|
|
148
|
+
/https?:\/\/[a-z0-9]+-[a-z0-9-]+-[a-z0-9]+\.tunnel-\d+\.tunnels\.meticulous\.ai/g,
|
|
149
|
+
/https?:\/\/[a-z0-9]+\.tunnel-\d+[^\s'")\]&]*/g,
|
|
150
|
+
/https?:\/\/[a-z0-9-]+\.lhr\.life[^\s'")\]&]*/g,
|
|
151
|
+
/https?:\/\/[a-z0-9-]+\.trycloudflare\.com[^\s'")\]&]*/g,
|
|
152
|
+
/X-Amz-Security-Token=[^\s&"']*/g,
|
|
153
|
+
/X-Amz-Credential=[^\s&"']*/g,
|
|
154
|
+
/X-Amz-Signature=[^\s&"']*/g,
|
|
155
|
+
/X-Amz-Date=[^\s&"']*/g,
|
|
156
|
+
/X-Amz-Expires=[^\s&"']*/g,
|
|
157
|
+
/"distinct_id":"[^"]*"/g,
|
|
158
|
+
/"token":"[^"]*"/g,
|
|
159
|
+
/"\$device_id":"[^"]*"/g,
|
|
160
|
+
/"\$session_id":"[^"]*"/g,
|
|
161
|
+
/"\$anon_distinct_id":"[^"]*"/g,
|
|
162
|
+
/"\$host":"[^"]*"/g,
|
|
163
|
+
/"\$current_url":"[^"]*"/g,
|
|
164
|
+
/"\$referrer":"[^"]*"/g,
|
|
165
|
+
/"\$initial_referrer":"[^"]*"/g,
|
|
166
|
+
/\/_next\/data\/[A-Za-z0-9_-]+\//g,
|
|
167
|
+
/\/_next\/static\/[A-Za-z0-9_-]{8,}\//g,
|
|
168
|
+
/(?<=[-./])[a-f0-9]{12,}(?=\.js)/g,
|
|
169
|
+
/(?<=[-./])[a-f0-9]{12,}(?=\.css)/g,
|
|
170
|
+
/(?<=["'/])[A-Za-z0-9_-]{20,}(?=["'/])/g,
|
|
171
|
+
];
|
|
172
|
+
const POSTHOG_LINE_PATTERNS = [
|
|
173
|
+
/us\.i\.posthog\.com/,
|
|
174
|
+
/\/decide\?/,
|
|
175
|
+
/\/e\?ip=1/,
|
|
176
|
+
/posthog-js/,
|
|
177
|
+
/\$autocapture/,
|
|
178
|
+
/\$pageview/,
|
|
179
|
+
/\$pageleave/,
|
|
180
|
+
/"event":"\$/,
|
|
181
|
+
];
|
|
182
|
+
const isPostHogLine = (content) => POSTHOG_LINE_PATTERNS.some((p) => p.test(content));
|
|
183
|
+
const filterLine = (line) => {
|
|
184
|
+
let result = line;
|
|
185
|
+
for (const pattern of LOG_NOISE_PATTERNS) {
|
|
186
|
+
result = result.replace(new RegExp(pattern.source, pattern.flags || "g"), "<NOISE>");
|
|
187
|
+
}
|
|
188
|
+
result = result.replace(/\brequest\s+\d+\b/gi, "request <N>");
|
|
189
|
+
return result;
|
|
190
|
+
};
|
|
191
|
+
const filterLogLines = (raw) => {
|
|
192
|
+
const lines = raw.split("\n");
|
|
193
|
+
const filtered = [];
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
if (isPostHogLine(line)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
filtered.push(filterLine(line));
|
|
199
|
+
}
|
|
200
|
+
return filtered.join("\n");
|
|
201
|
+
};
|
|
202
|
+
const generateLogDiffs = (debugContext, workspaceDir) => {
|
|
203
|
+
for (const diff of debugContext.replayDiffs) {
|
|
204
|
+
const headLogPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "head", diff.headReplayId, "logs.deterministic.txt");
|
|
205
|
+
const baseLogPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "base", diff.baseReplayId, "logs.deterministic.txt");
|
|
206
|
+
if (!(0, fs_1.existsSync)(headLogPath) || !(0, fs_1.existsSync)(baseLogPath)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const baseName = `${diff.headReplayId}-vs-${diff.baseReplayId}`;
|
|
210
|
+
generateLogDiffPair(baseLogPath, headLogPath, workspaceDir, baseName);
|
|
211
|
+
}
|
|
212
|
+
if (debugContext.replayDiffs.length === 0 &&
|
|
213
|
+
debugContext.replayIds.length === 2) {
|
|
214
|
+
const [idA, idB] = debugContext.replayIds;
|
|
215
|
+
const pathA = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "other", idA, "logs.deterministic.txt");
|
|
216
|
+
const pathB = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "other", idB, "logs.deterministic.txt");
|
|
217
|
+
if ((0, fs_1.existsSync)(pathA) && (0, fs_1.existsSync)(pathB)) {
|
|
218
|
+
generateLogDiffPair(pathA, pathB, workspaceDir, `${idA}-vs-${idB}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const generateLogDiffPair = (basePath, headPath, workspaceDir, baseName) => {
|
|
223
|
+
var _a;
|
|
224
|
+
const diffDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "log-diffs");
|
|
225
|
+
(0, fs_1.mkdirSync)(diffDir, { recursive: true });
|
|
226
|
+
let rawDiff;
|
|
227
|
+
try {
|
|
228
|
+
rawDiff = (0, child_process_1.execFileSync)("diff", ["-u", basePath, headPath], {
|
|
229
|
+
encoding: "utf8",
|
|
230
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
rawDiff = (_a = error === null || error === void 0 ? void 0 : error.stdout) !== null && _a !== void 0 ? _a : "";
|
|
235
|
+
}
|
|
236
|
+
if (!rawDiff.trim()) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(diffDir, `${baseName}.diff`), rawDiff);
|
|
240
|
+
const filteredDiff = filterLogDiff(rawDiff);
|
|
241
|
+
if (filteredDiff.trim()) {
|
|
242
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(diffDir, `${baseName}.filtered.diff`), filteredDiff);
|
|
243
|
+
}
|
|
244
|
+
const summary = summarizeLogDiff(rawDiff);
|
|
245
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(diffDir, `${baseName}.summary.txt`), summary);
|
|
246
|
+
};
|
|
247
|
+
const filterLogDiff = (rawDiff) => {
|
|
248
|
+
const lines = rawDiff.split("\n");
|
|
249
|
+
const filteredLines = [];
|
|
250
|
+
for (const line of lines) {
|
|
251
|
+
if (!line.startsWith("+") && !line.startsWith("-")) {
|
|
252
|
+
filteredLines.push(line);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
256
|
+
filteredLines.push(line);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (isPostHogLine(line)) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
filteredLines.push(filterLine(line));
|
|
263
|
+
}
|
|
264
|
+
return removeIdenticalHunks(filteredLines.join("\n"));
|
|
265
|
+
};
|
|
266
|
+
const removeIdenticalHunks = (diff) => {
|
|
267
|
+
var _a;
|
|
268
|
+
const parts = diff.split(/^(@@[^@]*@@.*$)/m);
|
|
269
|
+
const result = [parts[0]];
|
|
270
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
271
|
+
const hunkHeader = parts[i];
|
|
272
|
+
const hunkBody = (_a = parts[i + 1]) !== null && _a !== void 0 ? _a : "";
|
|
273
|
+
const addedLines = [];
|
|
274
|
+
const removedLines = [];
|
|
275
|
+
for (const line of hunkBody.split("\n")) {
|
|
276
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
277
|
+
addedLines.push(line.slice(1));
|
|
278
|
+
}
|
|
279
|
+
else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
280
|
+
removedLines.push(line.slice(1));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (addedLines.join("\n") !== removedLines.join("\n")) {
|
|
284
|
+
result.push(hunkHeader);
|
|
285
|
+
result.push(hunkBody);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return result.join("");
|
|
289
|
+
};
|
|
290
|
+
const summarizeLogDiff = (rawDiff) => {
|
|
291
|
+
const lines = rawDiff.split("\n");
|
|
292
|
+
let addedCount = 0;
|
|
293
|
+
let removedCount = 0;
|
|
294
|
+
let firstDivergenceLine;
|
|
295
|
+
const categories = {};
|
|
296
|
+
for (const line of lines) {
|
|
297
|
+
if (line.startsWith("@@")) {
|
|
298
|
+
const match = line.match(/@@ -(\d+)/);
|
|
299
|
+
if (match && firstDivergenceLine == null) {
|
|
300
|
+
firstDivergenceLine = parseInt(match[1], 10);
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
305
|
+
addedCount++;
|
|
306
|
+
categorizeLogLine(line, categories, "added");
|
|
307
|
+
}
|
|
308
|
+
else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
309
|
+
removedCount++;
|
|
310
|
+
categorizeLogLine(line, categories, "removed");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const sortedCategories = Object.entries(categories).sort(([, a], [, b]) => b.added + b.removed - (a.added + a.removed));
|
|
314
|
+
const parts = [
|
|
315
|
+
`Log Diff Summary`,
|
|
316
|
+
`================`,
|
|
317
|
+
``,
|
|
318
|
+
`Total changed lines: ${addedCount + removedCount} (+${addedCount} in head, -${removedCount} in base)`,
|
|
319
|
+
`First divergence at base log line: ${firstDivergenceLine !== null && firstDivergenceLine !== void 0 ? firstDivergenceLine : "unknown"}`,
|
|
320
|
+
``,
|
|
321
|
+
`Change categories (+ = extra in head, - = extra in base):`,
|
|
322
|
+
];
|
|
323
|
+
for (const [category, counts] of sortedCategories) {
|
|
324
|
+
const total = counts.added + counts.removed;
|
|
325
|
+
const net = counts.added - counts.removed;
|
|
326
|
+
const netStr = net > 0
|
|
327
|
+
? `net +${net} in head`
|
|
328
|
+
: net < 0
|
|
329
|
+
? `net ${net} in base`
|
|
330
|
+
: "balanced";
|
|
331
|
+
parts.push(` ${category}: ${total} lines (+${counts.added} / -${counts.removed}, ${netStr})`);
|
|
332
|
+
}
|
|
333
|
+
parts.push("", "Tip: Read the .filtered.diff file for a version with tunnel URLs,", "PostHog payloads, and S3 tokens stripped. Hunks that only differ", "in these noisy values are removed entirely.");
|
|
334
|
+
return parts.join("\n");
|
|
335
|
+
};
|
|
336
|
+
const categorizeLogLine = (line, categories, direction) => {
|
|
337
|
+
var _a;
|
|
338
|
+
const content = line.slice(1);
|
|
339
|
+
const category = classifyLogContent(content);
|
|
340
|
+
const entry = (_a = categories[category]) !== null && _a !== void 0 ? _a : { added: 0, removed: 0 };
|
|
341
|
+
entry[direction]++;
|
|
342
|
+
categories[category] = entry;
|
|
343
|
+
};
|
|
344
|
+
const classifyLogContent = (content) => {
|
|
345
|
+
if (LOG_NOISE_PATTERNS.some((p) => new RegExp(p.source).test(content))) {
|
|
346
|
+
return "noise (tunnel URLs / tokens / PostHog)";
|
|
347
|
+
}
|
|
348
|
+
if (/\bfetch\b|\bXHR\b|\bnetwork\b|\brequest\b|\bresponse\b/i.test(content)) {
|
|
349
|
+
return "network";
|
|
350
|
+
}
|
|
351
|
+
if (/animation|requestAnimationFrame|rAF/i.test(content)) {
|
|
352
|
+
return "animation frames";
|
|
353
|
+
}
|
|
354
|
+
if (/screenshot/i.test(content)) {
|
|
355
|
+
return "screenshots";
|
|
356
|
+
}
|
|
357
|
+
if (/timer|setTimeout|setInterval|tick/i.test(content)) {
|
|
358
|
+
return "timers";
|
|
359
|
+
}
|
|
360
|
+
if (/navigation|navigate|pushState|replaceState|popstate/i.test(content)) {
|
|
361
|
+
return "navigation";
|
|
362
|
+
}
|
|
363
|
+
if (/priority|ordering|reorder/i.test(content)) {
|
|
364
|
+
return "priority / ordering";
|
|
365
|
+
}
|
|
366
|
+
return "other";
|
|
367
|
+
};
|
|
368
|
+
const generateDiffSummaries = (workspaceDir) => {
|
|
369
|
+
var _a, _b;
|
|
370
|
+
const diffsDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "diffs");
|
|
371
|
+
if (!(0, fs_1.existsSync)(diffsDir)) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
let count = 0;
|
|
375
|
+
const jsonFiles = (0, fs_1.readdirSync)(diffsDir).filter((f) => f.endsWith(".json") && !f.endsWith(".summary.json"));
|
|
376
|
+
for (const file of jsonFiles) {
|
|
377
|
+
const raw = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(diffsDir, file), "utf8"));
|
|
378
|
+
const results = (_b = (_a = raw === null || raw === void 0 ? void 0 : raw.data) === null || _a === void 0 ? void 0 : _a.screenshotDiffResults) !== null && _b !== void 0 ? _b : [];
|
|
379
|
+
if (results.length === 0) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const compact = results.map((r) => {
|
|
383
|
+
const dtb = r.diffToBaseScreenshot;
|
|
384
|
+
return {
|
|
385
|
+
identifier: r.identifier,
|
|
386
|
+
outcome: r.outcome,
|
|
387
|
+
...(dtb
|
|
388
|
+
? {
|
|
389
|
+
width: dtb.width,
|
|
390
|
+
height: dtb.height,
|
|
391
|
+
mismatchPixels: dtb.mismatchPixels,
|
|
392
|
+
mismatchFraction: dtb.mismatchFraction,
|
|
393
|
+
mismatchPercent: dtb.mismatchFraction != null
|
|
394
|
+
? `${(dtb.mismatchFraction * 100).toFixed(4)}%`
|
|
395
|
+
: undefined,
|
|
396
|
+
diffFullFile: dtb.diffFullFile,
|
|
397
|
+
changedSectionsClassNames: dtb.changedSectionsClassNames,
|
|
398
|
+
}
|
|
399
|
+
: {}),
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
const summaryFile = file.replace(".json", ".summary.json");
|
|
403
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(diffsDir, summaryFile), JSON.stringify(compact, null, 2));
|
|
404
|
+
count++;
|
|
405
|
+
}
|
|
406
|
+
if (count > 0) {
|
|
407
|
+
console.log(chalk_1.default.green(` Generated ${count} compact diff summary(ies) in diffs/`));
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// PR diff from project repo
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
const generatePrDiff = (debugContext, workspaceDir) => {
|
|
414
|
+
var _a;
|
|
415
|
+
const projectRepoDir = (0, path_1.join)(workspaceDir, "project-repo");
|
|
416
|
+
if (!(0, fs_1.existsSync)(projectRepoDir)) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const headSha = debugContext.commitSha;
|
|
420
|
+
const baseSha = debugContext.baseCommitSha;
|
|
421
|
+
if (!headSha) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const effectiveBaseSha = baseSha !== null && baseSha !== void 0 ? baseSha : resolveBaseShaFromGit(projectRepoDir, headSha);
|
|
425
|
+
if (!effectiveBaseSha) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
let diffOutput;
|
|
429
|
+
try {
|
|
430
|
+
diffOutput = (0, child_process_1.execFileSync)("git", ["diff", `${effectiveBaseSha}..${headSha}`], { cwd: projectRepoDir, encoding: "utf8", maxBuffer: 50 * 1024 * 1024 });
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
diffOutput = (_a = error === null || error === void 0 ? void 0 : error.stdout) !== null && _a !== void 0 ? _a : "";
|
|
434
|
+
}
|
|
435
|
+
if (diffOutput.trim()) {
|
|
436
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "pr-diff.txt"), diffOutput);
|
|
437
|
+
console.log(chalk_1.default.green(` Generated PR diff (${effectiveBaseSha.slice(0, 8)}..${headSha.slice(0, 8)})`));
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const resolveBaseShaFromGit = (repoDir, headSha) => {
|
|
441
|
+
for (const branch of ["origin/main", "origin/master"]) {
|
|
442
|
+
try {
|
|
443
|
+
const mergeBase = (0, child_process_1.execFileSync)("git", ["merge-base", branch, headSha], { cwd: repoDir, encoding: "utf8" }).trim();
|
|
444
|
+
if (mergeBase) {
|
|
445
|
+
return mergeBase;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// Try next branch name
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return undefined;
|
|
453
|
+
};
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Params diffs
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
const generateParamsDiffs = (debugContext, workspaceDir) => {
|
|
458
|
+
var _a;
|
|
459
|
+
const paramsFile = "launchBrowserAndReplayParams.json";
|
|
460
|
+
const pairs = [];
|
|
461
|
+
for (const diff of debugContext.replayDiffs) {
|
|
462
|
+
pairs.push({
|
|
463
|
+
pathA: (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "base", diff.baseReplayId, paramsFile),
|
|
464
|
+
pathB: (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "head", diff.headReplayId, paramsFile),
|
|
465
|
+
label: `${diff.headReplayId}-vs-${diff.baseReplayId}`,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (debugContext.replayDiffs.length === 0 &&
|
|
469
|
+
debugContext.replayIds.length === 2) {
|
|
470
|
+
const [idA, idB] = debugContext.replayIds;
|
|
471
|
+
pairs.push({
|
|
472
|
+
pathA: (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "other", idA, paramsFile),
|
|
473
|
+
pathB: (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "other", idB, paramsFile),
|
|
474
|
+
label: `${idA}-vs-${idB}`,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
for (const { pathA, pathB, label } of pairs) {
|
|
478
|
+
if (!(0, fs_1.existsSync)(pathA) || !(0, fs_1.existsSync)(pathB)) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const jsonA = sortedJsonString((0, fs_1.readFileSync)(pathA, "utf8"));
|
|
483
|
+
const jsonB = sortedJsonString((0, fs_1.readFileSync)(pathB, "utf8"));
|
|
484
|
+
if (jsonA === jsonB) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const tmpA = (0, path_1.join)(workspaceDir, ".tmp-params-a.json");
|
|
488
|
+
const tmpB = (0, path_1.join)(workspaceDir, ".tmp-params-b.json");
|
|
489
|
+
(0, fs_1.writeFileSync)(tmpA, jsonA);
|
|
490
|
+
(0, fs_1.writeFileSync)(tmpB, jsonB);
|
|
491
|
+
let diffOutput;
|
|
492
|
+
try {
|
|
493
|
+
diffOutput = (0, child_process_1.execFileSync)("diff", ["-u", tmpA, tmpB], {
|
|
494
|
+
encoding: "utf8",
|
|
495
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch (diffError) {
|
|
499
|
+
diffOutput = (_a = diffError === null || diffError === void 0 ? void 0 : diffError.stdout) !== null && _a !== void 0 ? _a : "";
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
safeUnlink(tmpA);
|
|
503
|
+
safeUnlink(tmpB);
|
|
504
|
+
}
|
|
505
|
+
if (diffOutput.trim()) {
|
|
506
|
+
const diffsDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "params-diffs");
|
|
507
|
+
(0, fs_1.mkdirSync)(diffsDir, { recursive: true });
|
|
508
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(diffsDir, `${label}.diff`), diffOutput);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
513
|
+
console.warn(` Warning: Could not diff params for ${label}: ${message}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const diffsDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "params-diffs");
|
|
517
|
+
if ((0, fs_1.existsSync)(diffsDir) && (0, fs_1.readdirSync)(diffsDir).length > 0) {
|
|
518
|
+
console.log(chalk_1.default.green(" Generated replay params diff(s) in params-diffs/"));
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
const sortedJsonString = (raw) => {
|
|
522
|
+
const parsed = JSON.parse(raw);
|
|
523
|
+
return JSON.stringify(sortObjectKeys(parsed), null, 2);
|
|
524
|
+
};
|
|
525
|
+
const sortObjectKeys = (value) => {
|
|
526
|
+
if (value == null || typeof value !== "object") {
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
if (Array.isArray(value)) {
|
|
530
|
+
return value.map(sortObjectKeys);
|
|
531
|
+
}
|
|
532
|
+
const sorted = {};
|
|
533
|
+
for (const key of Object.keys(value).sort()) {
|
|
534
|
+
sorted[key] = sortObjectKeys(value[key]);
|
|
535
|
+
}
|
|
536
|
+
return sorted;
|
|
537
|
+
};
|
|
538
|
+
const safeUnlink = (filePath) => {
|
|
539
|
+
try {
|
|
540
|
+
(0, fs_1.unlinkSync)(filePath);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
// Non-critical
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
// Assets diffs
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
const generateAssetsDiff = (debugContext, workspaceDir) => {
|
|
550
|
+
for (const diff of debugContext.replayDiffs) {
|
|
551
|
+
const headAssetsDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "head", diff.headReplayId, "snapshotted-assets");
|
|
552
|
+
const baseAssetsDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "base", diff.baseReplayId, "snapshotted-assets");
|
|
553
|
+
const report = compareAssetDirs(baseAssetsDir, headAssetsDir);
|
|
554
|
+
if (!report) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const diffsDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "assets-diffs");
|
|
558
|
+
(0, fs_1.mkdirSync)(diffsDir, { recursive: true });
|
|
559
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(diffsDir, `${diff.headReplayId}-vs-${diff.baseReplayId}.txt`), report);
|
|
560
|
+
}
|
|
561
|
+
const diffsDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "assets-diffs");
|
|
562
|
+
if ((0, fs_1.existsSync)(diffsDir) && (0, fs_1.readdirSync)(diffsDir).length > 0) {
|
|
563
|
+
console.log(chalk_1.default.green(" Generated snapshotted assets diff(s) in assets-diffs/"));
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
const compareAssetDirs = (baseDir, headDir) => {
|
|
567
|
+
var _a;
|
|
568
|
+
const baseFiles = listAssetFiles(baseDir);
|
|
569
|
+
const headFiles = listAssetFiles(headDir);
|
|
570
|
+
if (baseFiles.size === 0 && headFiles.size === 0) {
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
573
|
+
const allNames = new Set([...baseFiles.keys(), ...headFiles.keys()]);
|
|
574
|
+
const added = [];
|
|
575
|
+
const removed = [];
|
|
576
|
+
const changed = [];
|
|
577
|
+
const unchanged = [];
|
|
578
|
+
for (const name of [...allNames].sort()) {
|
|
579
|
+
const baseInfo = baseFiles.get(name);
|
|
580
|
+
const headInfo = headFiles.get(name);
|
|
581
|
+
if (!baseInfo) {
|
|
582
|
+
added.push(` + ${name} (${(_a = headInfo === null || headInfo === void 0 ? void 0 : headInfo.size) !== null && _a !== void 0 ? _a : 0} bytes)`);
|
|
583
|
+
}
|
|
584
|
+
else if (!headInfo) {
|
|
585
|
+
removed.push(` - ${name} (${baseInfo.size} bytes)`);
|
|
586
|
+
}
|
|
587
|
+
else if (baseInfo.hash !== headInfo.hash) {
|
|
588
|
+
changed.push(` ~ ${name} (${baseInfo.size} -> ${headInfo.size} bytes)`);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
unchanged.push(name);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (added.length === 0 && removed.length === 0 && changed.length === 0) {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
const parts = [
|
|
598
|
+
"Snapshotted Assets Diff",
|
|
599
|
+
"=======================",
|
|
600
|
+
`Total: ${allNames.size} files (${added.length} added, ${removed.length} removed, ${changed.length} changed, ${unchanged.length} unchanged)`,
|
|
601
|
+
"",
|
|
602
|
+
];
|
|
603
|
+
if (added.length > 0) {
|
|
604
|
+
parts.push("Added:", ...added, "");
|
|
605
|
+
}
|
|
606
|
+
if (removed.length > 0) {
|
|
607
|
+
parts.push("Removed:", ...removed, "");
|
|
608
|
+
}
|
|
609
|
+
if (changed.length > 0) {
|
|
610
|
+
parts.push("Changed:", ...changed, "");
|
|
611
|
+
}
|
|
612
|
+
return parts.join("\n");
|
|
613
|
+
};
|
|
614
|
+
const listAssetFiles = (dir) => {
|
|
615
|
+
const result = new Map();
|
|
616
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
const walk = (currentDir, relativeTo) => {
|
|
620
|
+
const entries = (0, fs_1.readdirSync)(currentDir, { withFileTypes: true });
|
|
621
|
+
for (const entry of entries) {
|
|
622
|
+
const relativePath = relativeTo
|
|
623
|
+
? `${relativeTo}/${entry.name}`
|
|
624
|
+
: entry.name;
|
|
625
|
+
if (entry.isDirectory()) {
|
|
626
|
+
walk((0, path_1.join)(currentDir, entry.name), relativePath);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
const content = (0, fs_1.readFileSync)((0, path_1.join)(currentDir, entry.name));
|
|
630
|
+
const hash = (0, crypto_1.createHash)("md5").update(content).digest("hex");
|
|
631
|
+
result.set(relativePath, { size: content.length, hash });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
walk(dir, "");
|
|
636
|
+
return result;
|
|
637
|
+
};
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
// Timeline summaries
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
const generateTimelineSummaries = (workspaceDir) => {
|
|
642
|
+
const replaySubDirs = ["head", "base", "other"];
|
|
643
|
+
let count = 0;
|
|
644
|
+
for (const subDir of replaySubDirs) {
|
|
645
|
+
const subDirPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", subDir);
|
|
646
|
+
if (!(0, fs_1.existsSync)(subDirPath)) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
for (const replayId of (0, fs_1.readdirSync)(subDirPath)) {
|
|
650
|
+
const timelinePath = (0, path_1.join)(subDirPath, replayId, "timeline.json");
|
|
651
|
+
if (!(0, fs_1.existsSync)(timelinePath)) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const timeline = JSON.parse((0, fs_1.readFileSync)(timelinePath, "utf8"));
|
|
655
|
+
if (!Array.isArray(timeline)) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const summary = summarizeTimeline(timeline, replayId, subDir);
|
|
659
|
+
const summaryDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "timeline-summaries");
|
|
660
|
+
(0, fs_1.mkdirSync)(summaryDir, { recursive: true });
|
|
661
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(summaryDir, `${subDir}-${replayId}.txt`), summary);
|
|
662
|
+
count++;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (count > 0) {
|
|
666
|
+
console.log(chalk_1.default.green(` Generated ${count} timeline summary(ies) in timeline-summaries/`));
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
const summarizeTimeline = (timeline, replayId, role) => {
|
|
670
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
671
|
+
const eventKindCounts = {};
|
|
672
|
+
const screenshotEntries = [];
|
|
673
|
+
let navigationCount = 0;
|
|
674
|
+
let networkRequestCount = 0;
|
|
675
|
+
let animationFrameCount = 0;
|
|
676
|
+
let consoleMessageCount = 0;
|
|
677
|
+
let firstVirtualTime;
|
|
678
|
+
let lastVirtualTime;
|
|
679
|
+
const flakinessWarnings = [];
|
|
680
|
+
for (const entry of timeline) {
|
|
681
|
+
const kind = entry.kind;
|
|
682
|
+
eventKindCounts[kind] = ((_a = eventKindCounts[kind]) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
683
|
+
const vt = entry.virtualTimeStart;
|
|
684
|
+
if (vt != null) {
|
|
685
|
+
if (firstVirtualTime == null || vt < firstVirtualTime) {
|
|
686
|
+
firstVirtualTime = vt;
|
|
687
|
+
}
|
|
688
|
+
if (lastVirtualTime == null || vt > lastVirtualTime) {
|
|
689
|
+
lastVirtualTime = vt;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (kind.startsWith("potentialFlakinessWarning")) {
|
|
693
|
+
flakinessWarnings.push({
|
|
694
|
+
kind,
|
|
695
|
+
message: (_e = (_c = (_b = entry.data) === null || _b === void 0 ? void 0 : _b.message) !== null && _c !== void 0 ? _c : (_d = entry.data) === null || _d === void 0 ? void 0 : _d.warning) !== null && _e !== void 0 ? _e : JSON.stringify((_f = entry.data) !== null && _f !== void 0 ? _f : {}),
|
|
696
|
+
virtualTime: vt,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
switch (kind) {
|
|
700
|
+
case "screenshot": {
|
|
701
|
+
const filename = ((_g = entry.data) === null || _g === void 0 ? void 0 : _g.identifier)
|
|
702
|
+
? screenshotIdentifierToFilename(entry.data.identifier)
|
|
703
|
+
: undefined;
|
|
704
|
+
screenshotEntries.push({
|
|
705
|
+
filename: filename !== null && filename !== void 0 ? filename : "unknown",
|
|
706
|
+
virtualTime: vt,
|
|
707
|
+
});
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
case "initialNavigation":
|
|
711
|
+
case "navigation":
|
|
712
|
+
navigationCount++;
|
|
713
|
+
break;
|
|
714
|
+
case "pollyReplay":
|
|
715
|
+
networkRequestCount++;
|
|
716
|
+
break;
|
|
717
|
+
case "jsReplay":
|
|
718
|
+
animationFrameCount++;
|
|
719
|
+
break;
|
|
720
|
+
default:
|
|
721
|
+
if (kind.startsWith("consoleMessage")) {
|
|
722
|
+
consoleMessageCount++;
|
|
723
|
+
}
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const totalVirtualTimeMs = firstVirtualTime != null && lastVirtualTime != null
|
|
728
|
+
? lastVirtualTime - firstVirtualTime
|
|
729
|
+
: undefined;
|
|
730
|
+
const sortedKinds = Object.entries(eventKindCounts).sort(([, a], [, b]) => b - a);
|
|
731
|
+
const parts = [
|
|
732
|
+
`Timeline Summary: ${role}/${replayId}`,
|
|
733
|
+
"=".repeat(40 + role.length + replayId.length),
|
|
734
|
+
"",
|
|
735
|
+
`Total timeline entries: ${timeline.length}`,
|
|
736
|
+
`Total virtual time: ${totalVirtualTimeMs != null ? `${totalVirtualTimeMs}ms` : "unknown"}`,
|
|
737
|
+
`Virtual time range: ${firstVirtualTime !== null && firstVirtualTime !== void 0 ? firstVirtualTime : "?"} - ${lastVirtualTime !== null && lastVirtualTime !== void 0 ? lastVirtualTime : "?"}`,
|
|
738
|
+
"",
|
|
739
|
+
"Key counts:",
|
|
740
|
+
` Screenshots: ${screenshotEntries.length}`,
|
|
741
|
+
` Navigations: ${navigationCount}`,
|
|
742
|
+
` Network requests: ${networkRequestCount}`,
|
|
743
|
+
` Animation frames (jsReplay): ${animationFrameCount}`,
|
|
744
|
+
` Console messages: ${consoleMessageCount}`,
|
|
745
|
+
"",
|
|
746
|
+
"All event kinds:",
|
|
747
|
+
];
|
|
748
|
+
for (const [kind, count] of sortedKinds) {
|
|
749
|
+
parts.push(` ${kind}: ${count}`);
|
|
750
|
+
}
|
|
751
|
+
if (screenshotEntries.length > 0) {
|
|
752
|
+
parts.push("", "Screenshots:");
|
|
753
|
+
for (const ss of screenshotEntries) {
|
|
754
|
+
parts.push(` ${ss.filename} @ virtualTime ${ss.virtualTime != null ? `${ss.virtualTime}ms` : "unknown"}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (flakinessWarnings.length > 0) {
|
|
758
|
+
parts.push("", "Potential flakiness warnings:");
|
|
759
|
+
for (const fw of flakinessWarnings) {
|
|
760
|
+
parts.push(` [${fw.kind}] @ virtualTime ${fw.virtualTime != null ? `${fw.virtualTime}ms` : "unknown"}`);
|
|
761
|
+
parts.push(` ${fw.message}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return parts.join("\n");
|
|
765
|
+
};
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
// Prettify snapshot assets
|
|
768
|
+
// ---------------------------------------------------------------------------
|
|
769
|
+
const MAX_ASSET_SIZE_BYTES = 1024 * 1024;
|
|
770
|
+
const prettifySnapshotAssets = (workspaceDir) => {
|
|
771
|
+
const prettierPath = findPrettier();
|
|
772
|
+
if (!prettierPath) {
|
|
773
|
+
console.log(chalk_1.default.gray(" Skipping asset formatting (prettier not found on PATH)."));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const replaySubDirs = ["head", "base", "other"];
|
|
777
|
+
let copiedCount = 0;
|
|
778
|
+
let skippedLargeCount = 0;
|
|
779
|
+
for (const subDir of replaySubDirs) {
|
|
780
|
+
const subDirPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", subDir);
|
|
781
|
+
if (!(0, fs_1.existsSync)(subDirPath)) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
for (const replayId of (0, fs_1.readdirSync)(subDirPath)) {
|
|
785
|
+
const assetsDir = (0, path_1.join)(subDirPath, replayId, "snapshotted-assets");
|
|
786
|
+
if (!(0, fs_1.existsSync)(assetsDir)) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
let assetFiles;
|
|
790
|
+
try {
|
|
791
|
+
assetFiles = findAssetFilesRecursive(assetsDir, "");
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (assetFiles.length === 0) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const formattedDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "formatted-assets", subDir, replayId);
|
|
800
|
+
for (const relativePath of assetFiles) {
|
|
801
|
+
const srcPath = (0, path_1.join)(assetsDir, relativePath);
|
|
802
|
+
const fileSize = (0, fs_1.statSync)(srcPath).size;
|
|
803
|
+
if (fileSize > MAX_ASSET_SIZE_BYTES) {
|
|
804
|
+
skippedLargeCount++;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const destPath = (0, path_1.join)(formattedDir, relativePath);
|
|
808
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(destPath), { recursive: true });
|
|
809
|
+
(0, fs_1.copyFileSync)(srcPath, destPath);
|
|
810
|
+
copiedCount++;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (copiedCount === 0) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const formattedAssetsDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "formatted-assets");
|
|
818
|
+
try {
|
|
819
|
+
(0, child_process_1.execFileSync)(prettierPath, ["--write", `${formattedAssetsDir}/**/*.{js,css}`], {
|
|
820
|
+
timeout: 120000,
|
|
821
|
+
stdio: "ignore",
|
|
822
|
+
});
|
|
823
|
+
console.log(chalk_1.default.green(` Formatted ${copiedCount} snapshotted asset(s) in formatted-assets/`));
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
console.log(chalk_1.default.yellow(` Copied ${copiedCount} snapshotted asset(s) to formatted-assets/ (prettier formatting partially failed)`));
|
|
827
|
+
}
|
|
828
|
+
if (skippedLargeCount > 0) {
|
|
829
|
+
console.log(chalk_1.default.gray(` Skipped ${skippedLargeCount} large asset(s) over ${MAX_ASSET_SIZE_BYTES / (1024 * 1024)}MB`));
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
const findPrettier = () => {
|
|
833
|
+
try {
|
|
834
|
+
const whichResult = (0, child_process_1.execFileSync)("which", ["prettier"], {
|
|
835
|
+
encoding: "utf8",
|
|
836
|
+
}).trim();
|
|
837
|
+
if (whichResult) {
|
|
838
|
+
return whichResult;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
// Not on PATH
|
|
843
|
+
}
|
|
844
|
+
return undefined;
|
|
845
|
+
};
|
|
846
|
+
const findAssetFilesRecursive = (baseDir, relativeTo) => {
|
|
847
|
+
const results = [];
|
|
848
|
+
const entries = (0, fs_1.readdirSync)(baseDir, { withFileTypes: true });
|
|
849
|
+
for (const entry of entries) {
|
|
850
|
+
const relativePath = relativeTo
|
|
851
|
+
? `${relativeTo}/${entry.name}`
|
|
852
|
+
: entry.name;
|
|
853
|
+
if (entry.isDirectory()) {
|
|
854
|
+
results.push(...findAssetFilesRecursive((0, path_1.join)(baseDir, entry.name), relativePath));
|
|
855
|
+
}
|
|
856
|
+
else if (entry.name.endsWith(".js") || entry.name.endsWith(".css")) {
|
|
857
|
+
results.push(relativePath);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return results;
|
|
861
|
+
};
|
|
862
|
+
// ---------------------------------------------------------------------------
|
|
863
|
+
// Screenshot map & context
|
|
864
|
+
// ---------------------------------------------------------------------------
|
|
865
|
+
const SCREENSHOT_LOG_CONTEXT_LINES = 30;
|
|
866
|
+
const buildScreenshotMap = (debugContext, workspaceDir) => {
|
|
867
|
+
var _a, _b, _c, _d;
|
|
868
|
+
const map = {};
|
|
869
|
+
for (const subDir of ["head", "base", "other"]) {
|
|
870
|
+
const subDirPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", subDir);
|
|
871
|
+
if (!(0, fs_1.existsSync)(subDirPath)) {
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
for (const replayId of (0, fs_1.readdirSync)(subDirPath)) {
|
|
875
|
+
const timelinePath = (0, path_1.join)(subDirPath, replayId, "timeline.json");
|
|
876
|
+
if (!(0, fs_1.existsSync)(timelinePath)) {
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const timeline = JSON.parse((0, fs_1.readFileSync)(timelinePath, "utf8"));
|
|
880
|
+
if (!Array.isArray(timeline)) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
for (const entry of timeline) {
|
|
884
|
+
const e = entry;
|
|
885
|
+
if (e.kind !== "screenshot" || !((_a = e.data) === null || _a === void 0 ? void 0 : _a.identifier)) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
const filename = screenshotIdentifierToFilename(e.data.identifier);
|
|
889
|
+
if (!filename) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
map[`${subDir}/${replayId}/${filename}`] = {
|
|
893
|
+
replayId,
|
|
894
|
+
replayRole: subDir,
|
|
895
|
+
filename,
|
|
896
|
+
virtualTimeStart: (_b = e.virtualTimeStart) !== null && _b !== void 0 ? _b : null,
|
|
897
|
+
virtualTimeEnd: (_c = e.virtualTimeEnd) !== null && _c !== void 0 ? _c : null,
|
|
898
|
+
eventNumber: (_d = e.data.identifier.eventNumber) !== null && _d !== void 0 ? _d : null,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (Object.keys(map).length > 0) {
|
|
904
|
+
console.log(chalk_1.default.green(` Mapped ${Object.keys(map).length} screenshot(s) to virtual timestamps`));
|
|
905
|
+
}
|
|
906
|
+
return map;
|
|
907
|
+
};
|
|
908
|
+
const screenshotIdentifierToFilename = (identifier) => {
|
|
909
|
+
const variantPortion = identifier.variant === "redacted" ? ".redacted" : "";
|
|
910
|
+
if (identifier.type === "end-state") {
|
|
911
|
+
return identifier.logicVersion == null
|
|
912
|
+
? `final-state${variantPortion}.png`
|
|
913
|
+
: `final-state-v${identifier.logicVersion}${variantPortion}.png`;
|
|
914
|
+
}
|
|
915
|
+
if (identifier.type === "after-event" && identifier.eventNumber != null) {
|
|
916
|
+
const eventIndexStr = identifier.eventNumber.toString().padStart(5, "0");
|
|
917
|
+
return identifier.logicVersion == null
|
|
918
|
+
? `screenshot-after-event-${eventIndexStr}${variantPortion}.png`
|
|
919
|
+
: `screenshot-after-event-${eventIndexStr}-v${identifier.logicVersion}${variantPortion}.png`;
|
|
920
|
+
}
|
|
921
|
+
return undefined;
|
|
922
|
+
};
|
|
923
|
+
const generateScreenshotContext = (debugContext, workspaceDir, screenshotMap) => {
|
|
924
|
+
var _a, _b, _c, _d;
|
|
925
|
+
if (!debugContext.screenshot) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const targetScreenshot = debugContext.screenshot;
|
|
929
|
+
let count = 0;
|
|
930
|
+
for (const diff of debugContext.replayDiffs) {
|
|
931
|
+
const headLogPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "head", diff.headReplayId, "logs.deterministic.txt");
|
|
932
|
+
const baseLogPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", "base", diff.baseReplayId, "logs.deterministic.txt");
|
|
933
|
+
const headEntry = screenshotMap[`head/${diff.headReplayId}/${targetScreenshot}`];
|
|
934
|
+
const baseEntry = screenshotMap[`base/${diff.baseReplayId}/${targetScreenshot}`];
|
|
935
|
+
const parts = [
|
|
936
|
+
`Screenshot Context: ${targetScreenshot}`,
|
|
937
|
+
"=".repeat(40 + targetScreenshot.length),
|
|
938
|
+
"",
|
|
939
|
+
];
|
|
940
|
+
if (headEntry) {
|
|
941
|
+
parts.push(`HEAD (${diff.headReplayId}) @ virtualTime ${(_a = headEntry.virtualTimeStart) !== null && _a !== void 0 ? _a : "?"}ms, event ${(_b = headEntry.eventNumber) !== null && _b !== void 0 ? _b : "?"}:`, "");
|
|
942
|
+
const headContext = extractLogContext(headLogPath, headEntry);
|
|
943
|
+
parts.push(headContext !== null && headContext !== void 0 ? headContext : " (could not extract log context)", "");
|
|
944
|
+
}
|
|
945
|
+
if (baseEntry) {
|
|
946
|
+
parts.push(`BASE (${diff.baseReplayId}) @ virtualTime ${(_c = baseEntry.virtualTimeStart) !== null && _c !== void 0 ? _c : "?"}ms, event ${(_d = baseEntry.eventNumber) !== null && _d !== void 0 ? _d : "?"}:`, "");
|
|
947
|
+
const baseContext = extractLogContext(baseLogPath, baseEntry);
|
|
948
|
+
parts.push(baseContext !== null && baseContext !== void 0 ? baseContext : " (could not extract log context)", "");
|
|
949
|
+
}
|
|
950
|
+
if (headEntry || baseEntry) {
|
|
951
|
+
const contextDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "screenshot-context");
|
|
952
|
+
(0, fs_1.mkdirSync)(contextDir, { recursive: true });
|
|
953
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(contextDir, `${diff.headReplayId}-vs-${diff.baseReplayId}-${targetScreenshot.replace(".png", "")}.txt`), parts.join("\n"));
|
|
954
|
+
count++;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (count > 0) {
|
|
958
|
+
console.log(chalk_1.default.green(` Generated screenshot context for ${targetScreenshot} in screenshot-context/`));
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
const extractLogContext = (logPath, entry) => {
|
|
962
|
+
if (!(0, fs_1.existsSync)(logPath) || entry.virtualTimeStart == null) {
|
|
963
|
+
return undefined;
|
|
964
|
+
}
|
|
965
|
+
const lines = (0, fs_1.readFileSync)(logPath, "utf8").split("\n");
|
|
966
|
+
const targetVt = entry.virtualTimeStart;
|
|
967
|
+
let bestLine = -1;
|
|
968
|
+
let bestDist = Infinity;
|
|
969
|
+
for (let i = 0; i < lines.length; i++) {
|
|
970
|
+
if (!/screenshot/i.test(lines[i])) {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const vtMatch = lines[i].match(/\[virtual:\s*([\d.]+)ms\]/);
|
|
974
|
+
if (!vtMatch) {
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
const vt = parseFloat(vtMatch[1]);
|
|
978
|
+
const dist = Math.abs(vt - targetVt);
|
|
979
|
+
if (dist < bestDist) {
|
|
980
|
+
bestDist = dist;
|
|
981
|
+
bestLine = i;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (bestLine < 0) {
|
|
985
|
+
return undefined;
|
|
986
|
+
}
|
|
987
|
+
const start = Math.max(0, bestLine - SCREENSHOT_LOG_CONTEXT_LINES);
|
|
988
|
+
const end = Math.min(lines.length, bestLine + SCREENSHOT_LOG_CONTEXT_LINES + 1);
|
|
989
|
+
const contextLines = lines.slice(start, end);
|
|
990
|
+
contextLines[bestLine - start] = `>>> ${contextLines[bestLine - start]}`;
|
|
991
|
+
return contextLines.join("\n");
|
|
992
|
+
};
|
|
993
|
+
// ---------------------------------------------------------------------------
|
|
994
|
+
// Replay comparison
|
|
995
|
+
// ---------------------------------------------------------------------------
|
|
996
|
+
const buildReplayComparison = (debugContext, workspaceDir) => {
|
|
997
|
+
const entries = [];
|
|
998
|
+
for (const subDir of ["head", "base", "other"]) {
|
|
999
|
+
const subDirPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", subDir);
|
|
1000
|
+
if (!(0, fs_1.existsSync)(subDirPath)) {
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
for (const replayId of (0, fs_1.readdirSync)(subDirPath)) {
|
|
1004
|
+
const replayDir = (0, path_1.join)(subDirPath, replayId);
|
|
1005
|
+
const stats = extractReplayStats(replayDir);
|
|
1006
|
+
entries.push({ replayId, role: subDir, ...stats });
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (entries.length >= 2) {
|
|
1010
|
+
console.log(chalk_1.default.green(` Generated replay comparison for ${entries.length} replay(s)`));
|
|
1011
|
+
}
|
|
1012
|
+
return entries;
|
|
1013
|
+
};
|
|
1014
|
+
const extractReplayStats = (replayDir) => {
|
|
1015
|
+
var _a, _b, _c;
|
|
1016
|
+
const stats = {
|
|
1017
|
+
totalEvents: null,
|
|
1018
|
+
totalNetworkRequests: null,
|
|
1019
|
+
totalAnimationFrames: null,
|
|
1020
|
+
totalVirtualTimeMs: null,
|
|
1021
|
+
screenshotCount: null,
|
|
1022
|
+
};
|
|
1023
|
+
const timelineStatsPath = (0, path_1.join)(replayDir, "timeline-stats.json");
|
|
1024
|
+
if ((0, fs_1.existsSync)(timelineStatsPath)) {
|
|
1025
|
+
const timelineStats = JSON.parse((0, fs_1.readFileSync)(timelineStatsPath, "utf8"));
|
|
1026
|
+
const counts = (_a = timelineStats.countByType) !== null && _a !== void 0 ? _a : {};
|
|
1027
|
+
stats.totalEvents = Object.values(counts).reduce((sum, n) => sum + n, 0);
|
|
1028
|
+
stats.totalNetworkRequests = (_b = counts["pollyReplay"]) !== null && _b !== void 0 ? _b : null;
|
|
1029
|
+
stats.totalAnimationFrames = (_c = counts["jsReplay"]) !== null && _c !== void 0 ? _c : null;
|
|
1030
|
+
}
|
|
1031
|
+
const timelinePath = (0, path_1.join)(replayDir, "timeline.json");
|
|
1032
|
+
if ((0, fs_1.existsSync)(timelinePath)) {
|
|
1033
|
+
const timeline = JSON.parse((0, fs_1.readFileSync)(timelinePath, "utf8"));
|
|
1034
|
+
if (Array.isArray(timeline) && timeline.length > 0) {
|
|
1035
|
+
let minVt;
|
|
1036
|
+
let maxVt;
|
|
1037
|
+
for (const entry of timeline) {
|
|
1038
|
+
const vt = entry.virtualTimeStart;
|
|
1039
|
+
if (vt != null) {
|
|
1040
|
+
if (minVt == null || vt < minVt) {
|
|
1041
|
+
minVt = vt;
|
|
1042
|
+
}
|
|
1043
|
+
if (maxVt == null || vt > maxVt) {
|
|
1044
|
+
maxVt = vt;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (minVt != null && maxVt != null) {
|
|
1049
|
+
stats.totalVirtualTimeMs = Math.round(maxVt - minVt);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const replayId = (0, path_1.basename)(replayDir);
|
|
1054
|
+
const screenshotsDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "replays", replayId, "screenshots");
|
|
1055
|
+
if ((0, fs_1.existsSync)(screenshotsDir)) {
|
|
1056
|
+
stats.screenshotCount = (0, fs_1.readdirSync)(screenshotsDir).filter((f) => f.endsWith(".png")).length;
|
|
1057
|
+
}
|
|
1058
|
+
return stats;
|
|
1059
|
+
};
|
|
1060
|
+
// ---------------------------------------------------------------------------
|
|
1061
|
+
// File metadata
|
|
1062
|
+
// ---------------------------------------------------------------------------
|
|
1063
|
+
const collectFileMetadata = (debugContext, workspaceDir) => {
|
|
1064
|
+
const entries = [];
|
|
1065
|
+
const replayFiles = [
|
|
1066
|
+
"logs.deterministic.txt",
|
|
1067
|
+
"logs.deterministic.filtered.txt",
|
|
1068
|
+
"logs.concise.txt",
|
|
1069
|
+
"timeline.json",
|
|
1070
|
+
"timeline-stats.json",
|
|
1071
|
+
"metadata.json",
|
|
1072
|
+
"launchBrowserAndReplayParams.json",
|
|
1073
|
+
"stackTraces.json",
|
|
1074
|
+
"accuracyData.json",
|
|
1075
|
+
];
|
|
1076
|
+
for (const subDir of ["head", "base", "other"]) {
|
|
1077
|
+
const subDirPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", subDir);
|
|
1078
|
+
if (!(0, fs_1.existsSync)(subDirPath)) {
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
for (const replayId of (0, fs_1.readdirSync)(subDirPath)) {
|
|
1082
|
+
for (const fileName of replayFiles) {
|
|
1083
|
+
const filePath = (0, path_1.join)(subDirPath, replayId, fileName);
|
|
1084
|
+
const relativePath = `${debug_constants_1.DEBUG_DATA_DIRECTORY}/replays/${subDir}/${replayId}/${fileName}`;
|
|
1085
|
+
const meta = getFileMetadata(filePath, relativePath);
|
|
1086
|
+
if (meta) {
|
|
1087
|
+
entries.push(meta);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
for (const sessionId of debugContext.sessionIds) {
|
|
1093
|
+
const filePath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "sessions", sessionId, "data.json");
|
|
1094
|
+
const meta = getFileMetadata(filePath, `${debug_constants_1.DEBUG_DATA_DIRECTORY}/sessions/${sessionId}/data.json`);
|
|
1095
|
+
if (meta) {
|
|
1096
|
+
entries.push(meta);
|
|
1097
|
+
}
|
|
1098
|
+
const summaryPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "session-summaries", `${sessionId}.txt`);
|
|
1099
|
+
const summaryMeta = getFileMetadata(summaryPath, `${debug_constants_1.DEBUG_DATA_DIRECTORY}/session-summaries/${sessionId}.txt`);
|
|
1100
|
+
if (summaryMeta) {
|
|
1101
|
+
entries.push(summaryMeta);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return entries;
|
|
1105
|
+
};
|
|
1106
|
+
const getFileMetadata = (filePath, relativePath) => {
|
|
1107
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
1108
|
+
return undefined;
|
|
1109
|
+
}
|
|
1110
|
+
const buf = (0, fs_1.readFileSync)(filePath);
|
|
1111
|
+
let lines = 0;
|
|
1112
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1113
|
+
if (buf[i] === 0x0a) {
|
|
1114
|
+
lines++;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return { path: relativePath, bytes: buf.length, lines };
|
|
1118
|
+
};
|
|
1119
|
+
const generateSessionSummaries = (debugContext, workspaceDir) => {
|
|
1120
|
+
let count = 0;
|
|
1121
|
+
for (const sessionId of debugContext.sessionIds) {
|
|
1122
|
+
const dataPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "sessions", sessionId, "data.json");
|
|
1123
|
+
if (!(0, fs_1.existsSync)(dataPath)) {
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
try {
|
|
1127
|
+
const raw = JSON.parse((0, fs_1.readFileSync)(dataPath, "utf8"));
|
|
1128
|
+
const summary = summarizeSession(raw, sessionId);
|
|
1129
|
+
const summaryDir = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "session-summaries");
|
|
1130
|
+
(0, fs_1.mkdirSync)(summaryDir, { recursive: true });
|
|
1131
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(summaryDir, `${sessionId}.txt`), summary);
|
|
1132
|
+
count++;
|
|
1133
|
+
}
|
|
1134
|
+
catch (error) {
|
|
1135
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1136
|
+
console.warn(` Warning: Could not summarize session ${sessionId}: ${message}`);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (count > 0) {
|
|
1140
|
+
console.log(chalk_1.default.green(` Generated ${count} session summary(ies) in session-summaries/`));
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
const summarizeSession = (data, sessionId) => {
|
|
1144
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
|
|
1145
|
+
const parts = [
|
|
1146
|
+
`Session Summary: ${sessionId}`,
|
|
1147
|
+
"=".repeat(20 + sessionId.length),
|
|
1148
|
+
"",
|
|
1149
|
+
];
|
|
1150
|
+
const win = (_a = data.userEvents) === null || _a === void 0 ? void 0 : _a.window;
|
|
1151
|
+
parts.push(`Window: ${(_b = win === null || win === void 0 ? void 0 : win.startUrl) !== null && _b !== void 0 ? _b : "unknown"}`);
|
|
1152
|
+
if ((win === null || win === void 0 ? void 0 : win.width) != null && (win === null || win === void 0 ? void 0 : win.height) != null) {
|
|
1153
|
+
parts.push(`Viewport: ${win.width} x ${win.height}`);
|
|
1154
|
+
}
|
|
1155
|
+
parts.push(`Hostname: ${(_c = data.hostname) !== null && _c !== void 0 ? _c : "unknown"}`);
|
|
1156
|
+
parts.push(`Recorded: ${(_d = data.datetime_first_payload) !== null && _d !== void 0 ? _d : "unknown"}`);
|
|
1157
|
+
parts.push(`Abandoned: ${data.abandoned ? "yes" : "no"}`);
|
|
1158
|
+
parts.push("");
|
|
1159
|
+
const history = (_e = data.urlHistory) !== null && _e !== void 0 ? _e : [];
|
|
1160
|
+
if (history.length > 0) {
|
|
1161
|
+
parts.push(`URL History (${history.length} pages):`);
|
|
1162
|
+
for (const entry of history) {
|
|
1163
|
+
const ts = entry.timestamp != null ? `${entry.timestamp}ms` : "?";
|
|
1164
|
+
const pattern = entry.urlPattern
|
|
1165
|
+
? ` [pattern: ${entry.urlPattern}]`
|
|
1166
|
+
: "";
|
|
1167
|
+
parts.push(` ${ts} ${(_f = entry.url) !== null && _f !== void 0 ? _f : "unknown"}${pattern}`);
|
|
1168
|
+
}
|
|
1169
|
+
parts.push("");
|
|
1170
|
+
}
|
|
1171
|
+
const events = (_h = (_g = data.userEvents) === null || _g === void 0 ? void 0 : _g.event_log) !== null && _h !== void 0 ? _h : [];
|
|
1172
|
+
if (events.length > 0) {
|
|
1173
|
+
const typeCounts = {};
|
|
1174
|
+
for (const event of events) {
|
|
1175
|
+
typeCounts[(_j = event.type) !== null && _j !== void 0 ? _j : "unknown"] =
|
|
1176
|
+
((_l = typeCounts[(_k = event.type) !== null && _k !== void 0 ? _k : "unknown"]) !== null && _l !== void 0 ? _l : 0) + 1;
|
|
1177
|
+
}
|
|
1178
|
+
parts.push(`User Events: ${events.length} events`);
|
|
1179
|
+
for (const [type, count] of Object.entries(typeCounts).sort(([, a], [, b]) => b - a)) {
|
|
1180
|
+
parts.push(` ${type}: ${count}`);
|
|
1181
|
+
}
|
|
1182
|
+
parts.push("");
|
|
1183
|
+
}
|
|
1184
|
+
const harContainer = (_m = data.pollyHAR) === null || _m === void 0 ? void 0 : _m.pollyHAR;
|
|
1185
|
+
if (harContainer) {
|
|
1186
|
+
const allEntries = Object.values(harContainer).flatMap((r) => { var _a, _b; return (_b = (_a = r === null || r === void 0 ? void 0 : r.log) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : []; });
|
|
1187
|
+
if (allEntries.length > 0) {
|
|
1188
|
+
parts.push(`Network Requests: ${allEntries.length} total`);
|
|
1189
|
+
const methodCounts = {};
|
|
1190
|
+
for (const entry of allEntries) {
|
|
1191
|
+
const method = (_p = (_o = entry.request) === null || _o === void 0 ? void 0 : _o.method) !== null && _p !== void 0 ? _p : "UNKNOWN";
|
|
1192
|
+
methodCounts[method] = ((_q = methodCounts[method]) !== null && _q !== void 0 ? _q : 0) + 1;
|
|
1193
|
+
}
|
|
1194
|
+
parts.push(` Methods: ${Object.entries(methodCounts)
|
|
1195
|
+
.sort(([, a], [, b]) => b - a)
|
|
1196
|
+
.map(([m, c]) => `${m} ${c}`)
|
|
1197
|
+
.join(", ")}`);
|
|
1198
|
+
parts.push("");
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const localStorage = (_u = (_t = (_s = (_r = data.randomEvents) === null || _r === void 0 ? void 0 : _r.localStorage) === null || _s === void 0 ? void 0 : _s.state) === null || _t === void 0 ? void 0 : _t.length) !== null && _u !== void 0 ? _u : 0;
|
|
1202
|
+
const sessionStorage = (_y = (_x = (_w = (_v = data.randomEvents) === null || _v === void 0 ? void 0 : _v.sessionStorage) === null || _w === void 0 ? void 0 : _w.state) === null || _x === void 0 ? void 0 : _x.length) !== null && _y !== void 0 ? _y : 0;
|
|
1203
|
+
const cookies = (_0 = (_z = data.cookies) === null || _z === void 0 ? void 0 : _z.length) !== null && _0 !== void 0 ? _0 : 0;
|
|
1204
|
+
parts.push("Storage:");
|
|
1205
|
+
parts.push(` localStorage: ${localStorage} entries`);
|
|
1206
|
+
parts.push(` sessionStorage: ${sessionStorage} entries`);
|
|
1207
|
+
parts.push(` Cookies: ${cookies}`);
|
|
1208
|
+
parts.push("");
|
|
1209
|
+
const connections = (_1 = data.webSocketData) !== null && _1 !== void 0 ? _1 : [];
|
|
1210
|
+
if (connections.length > 0) {
|
|
1211
|
+
parts.push(`WebSocket Connections: ${connections.length}`);
|
|
1212
|
+
for (const conn of connections) {
|
|
1213
|
+
parts.push(` ${(_2 = conn.url) !== null && _2 !== void 0 ? _2 : "unknown"} (${(_4 = (_3 = conn.events) === null || _3 === void 0 ? void 0 : _3.length) !== null && _4 !== void 0 ? _4 : 0} events)`);
|
|
1214
|
+
}
|
|
1215
|
+
parts.push("");
|
|
1216
|
+
}
|
|
1217
|
+
const ctx = data.context;
|
|
1218
|
+
if (ctx) {
|
|
1219
|
+
parts.push("Session Context:");
|
|
1220
|
+
if (ctx.userId || ctx.userEmail) {
|
|
1221
|
+
parts.push(` User: ${[ctx.userId, ctx.userEmail].filter(Boolean).join(" / ")}`);
|
|
1222
|
+
}
|
|
1223
|
+
const flags = Object.entries((_5 = ctx.featureFlags) !== null && _5 !== void 0 ? _5 : {});
|
|
1224
|
+
if (flags.length > 0) {
|
|
1225
|
+
parts.push(` Feature flags: ${flags.map(([k, v]) => `${k}=${v}`).join(", ")}`);
|
|
1226
|
+
}
|
|
1227
|
+
parts.push("");
|
|
1228
|
+
}
|
|
1229
|
+
const appData = data.applicationSpecificData;
|
|
1230
|
+
if (appData === null || appData === void 0 ? void 0 : appData.nextJs) {
|
|
1231
|
+
parts.push("Framework: Next.js");
|
|
1232
|
+
if (appData.nextJs.page) {
|
|
1233
|
+
parts.push(` Page: ${appData.nextJs.page}`);
|
|
1234
|
+
}
|
|
1235
|
+
parts.push("");
|
|
1236
|
+
}
|
|
1237
|
+
else if (appData === null || appData === void 0 ? void 0 : appData.reactRouter) {
|
|
1238
|
+
parts.push("Framework: React Router");
|
|
1239
|
+
parts.push("");
|
|
1240
|
+
}
|
|
1241
|
+
return parts.join("\n");
|
|
1242
|
+
};
|
|
1243
|
+
// ---------------------------------------------------------------------------
|
|
1244
|
+
// Context JSON generation (default implementation)
|
|
1245
|
+
// ---------------------------------------------------------------------------
|
|
1246
|
+
const defaultWriteContextJson = (debugContext, workspaceDir, fileMetadata, projectRepoDir, screenshotMap, replayComparison) => {
|
|
1247
|
+
const headIds = new Set(debugContext.replayDiffs.map((d) => d.headReplayId));
|
|
1248
|
+
const baseIds = new Set(debugContext.replayDiffs.map((d) => d.baseReplayId));
|
|
1249
|
+
const headReplays = [];
|
|
1250
|
+
const baseReplays = [];
|
|
1251
|
+
const otherReplays = [];
|
|
1252
|
+
for (const id of debugContext.replayIds) {
|
|
1253
|
+
if (headIds.has(id)) {
|
|
1254
|
+
headReplays.push(id);
|
|
1255
|
+
}
|
|
1256
|
+
else if (baseIds.has(id)) {
|
|
1257
|
+
baseReplays.push(id);
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
otherReplays.push(id);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
const context = {
|
|
1264
|
+
createdAt: new Date().toISOString(),
|
|
1265
|
+
orgProject: debugContext.orgAndProject,
|
|
1266
|
+
testRunId: debugContext.testRunId,
|
|
1267
|
+
testRunStatus: debugContext.testRunStatus,
|
|
1268
|
+
commitSha: debugContext.commitSha,
|
|
1269
|
+
baseCommitSha: debugContext.baseCommitSha,
|
|
1270
|
+
screenshot: debugContext.screenshot,
|
|
1271
|
+
replayDiffs: debugContext.replayDiffs.map((d) => ({
|
|
1272
|
+
id: d.id,
|
|
1273
|
+
headReplayId: d.headReplayId,
|
|
1274
|
+
baseReplayId: d.baseReplayId,
|
|
1275
|
+
sessionId: d.sessionId,
|
|
1276
|
+
numScreenshotDiffs: d.numScreenshotDiffs,
|
|
1277
|
+
})),
|
|
1278
|
+
replays: { head: headReplays, base: baseReplays, other: otherReplays },
|
|
1279
|
+
sessions: debugContext.sessionIds,
|
|
1280
|
+
screenshotMap,
|
|
1281
|
+
replayComparison,
|
|
1282
|
+
paths: {
|
|
1283
|
+
replays: "replays/",
|
|
1284
|
+
sessions: "sessions/",
|
|
1285
|
+
diffs: "diffs/",
|
|
1286
|
+
logDiffs: "log-diffs/",
|
|
1287
|
+
logDiffsFiltered: "log-diffs/*.filtered.diff",
|
|
1288
|
+
logDiffsSummary: "log-diffs/*.summary.txt",
|
|
1289
|
+
paramsDiffs: "params-diffs/",
|
|
1290
|
+
assetsDiffs: "assets-diffs/",
|
|
1291
|
+
timelineSummaries: "timeline-summaries/",
|
|
1292
|
+
screenshotContext: "screenshot-context/",
|
|
1293
|
+
prDiff: "pr-diff.txt",
|
|
1294
|
+
formattedAssets: "formatted-assets/",
|
|
1295
|
+
testRun: "test-run/",
|
|
1296
|
+
projectRepo: projectRepoDir ? "project-repo/" : undefined,
|
|
1297
|
+
},
|
|
1298
|
+
fileMetadata,
|
|
1299
|
+
};
|
|
1300
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "context.json"), JSON.stringify(context, null, 2));
|
|
1301
|
+
};
|
|
1302
|
+
//# sourceMappingURL=generate-debug-workspace.js.map
|