devtools-tracing 1.5.0 → 1.6.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.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/cli.ts +29 -5
  3. package/dist/cli.js +105 -9
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -31,7 +31,7 @@ npx devtools-tracing inp trace.json.gz
31
31
  npx devtools-tracing stats trace.json.gz
32
32
 
33
33
  # Source map symbolication (writes a .symbolicated.json.gz alongside the input)
34
- npx devtools-tracing sourcemap trace.json.gz
34
+ npx devtools-tracing sourcemap trace.json.gz -H "Cookie: $cookie" -H "User-Agent: $ua"
35
35
  ```
36
36
 
37
37
  See the [`commands/`](./commands) directory for full source.
package/cli.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { run as cssSelectors } from './commands/selector-stats.js';
2
2
  import { run as inp } from './commands/inp.js';
3
- import { run as sourcemap } from './commands/sourcemap.js';
3
+ import { run as sourcemap, type SourcemapOptions } from './commands/sourcemap.js';
4
4
  import { run as stats } from './commands/stats.js';
5
5
 
6
- const commands: Record<string, (tracePath: string) => Promise<void>> = {
6
+ const commands: Record<string, (tracePath: string, ...args: any[]) => Promise<void>> = {
7
7
  'selector-stats': cssSelectors,
8
8
  inp,
9
9
  sourcemap,
@@ -15,12 +15,30 @@ function printUsage() {
15
15
  console.log('\nCommands:');
16
16
  console.log(' selector-stats Top CSS selectors from selector stats and invalidation tracking');
17
17
  console.log(' inp Extract INP (Interaction to Next Paint) breakdown');
18
- console.log(' sourcemap Symbolicate a trace using source maps');
18
+ console.log(' sourcemap Symbolicate a trace using source maps [-H "Header: value"]');
19
19
  console.log(' stats Generate timeline category statistics');
20
20
  }
21
21
 
22
+ function parseHeaders(args: string[]): Record<string, string> {
23
+ const headers: Record<string, string> = {};
24
+ for (let i = 0; i < args.length; i++) {
25
+ if (args[i] === '-H' && i + 1 < args.length) {
26
+ const value = args[++i];
27
+ const colonIdx = value.indexOf(':');
28
+ if (colonIdx === -1) {
29
+ console.error(`Invalid header (missing ':'): ${value}`);
30
+ process.exit(1);
31
+ }
32
+ headers[value.slice(0, colonIdx).trim()] = value.slice(colonIdx + 1).trim();
33
+ }
34
+ }
35
+ return headers;
36
+ }
37
+
22
38
  async function main() {
23
- const [command, tracePath] = process.argv.slice(2);
39
+ const args = process.argv.slice(2);
40
+ const command = args[0];
41
+ const tracePath = args[1];
24
42
 
25
43
  if (!command || command === '--help' || command === '-h') {
26
44
  printUsage();
@@ -40,7 +58,13 @@ async function main() {
40
58
  process.exit(1);
41
59
  }
42
60
 
43
- await run(tracePath);
61
+ if (command === 'sourcemap') {
62
+ const headers = parseHeaders(args.slice(2));
63
+ const options: SourcemapOptions = Object.keys(headers).length > 0 ? { headers } : {};
64
+ await sourcemap(tracePath, options);
65
+ } else {
66
+ await run(tracePath);
67
+ }
44
68
  }
45
69
 
46
70
  main();
package/dist/cli.js CHANGED
@@ -176,8 +176,9 @@ async function run2(tracePath) {
176
176
  var fs3 = __toESM(require("node:fs"));
177
177
  var zlib3 = __toESM(require("node:zlib"));
178
178
  var import__3 = require("../");
179
- async function run3(tracePath) {
179
+ async function run3(tracePath, options) {
180
180
  (0, import__3.initDevToolsTracing)();
181
+ const extraHeaders = options?.headers;
181
182
  const fileData = fs3.readFileSync(tracePath);
182
183
  const decompressedData = tracePath.endsWith(".gz") ? zlib3.gunzipSync(fileData) : fileData;
183
184
  const traceData = JSON.parse(
@@ -185,9 +186,10 @@ async function run3(tracePath) {
185
186
  );
186
187
  const traceModel = import__3.Trace.TraceModel.Model.createWithAllHandlers();
187
188
  const resolveSourceMap = (0, import__3.createSourceMapResolver)({
188
- fetch: verboseFetch
189
+ fetch: (url) => verboseFetch(url, extraHeaders)
189
190
  });
190
- await traceModel.parse(traceData.traceEvents, {
191
+ const preprocessedEvents = fixupElectronTraceEvents(traceData.traceEvents);
192
+ await traceModel.parse(preprocessedEvents, {
191
193
  isCPUProfile: false,
192
194
  isFreshRecording: false,
193
195
  metadata: traceData.metadata,
@@ -238,7 +240,7 @@ async function run3(tracePath) {
238
240
  console.log(
239
241
  `Symbolicated ${result.symbolicatedFrames} frames across ${result.symbolicatedEvents} events`
240
242
  );
241
- const outPath = tracePath.replace(/(\.(json|json\.gz))$/i, ".symbolicated$1");
243
+ const outPath = tracePath.replace(/(\.(json|trace|json\.gz))$/i, ".symbolicated$1");
242
244
  if (outPath === tracePath) {
243
245
  console.error(
244
246
  "Could not determine output path (expected .json or .json.gz extension)"
@@ -253,9 +255,80 @@ async function run3(tracePath) {
253
255
  }
254
256
  console.log(`Wrote symbolicated trace to ${outPath}`);
255
257
  }
256
- async function verboseFetch(url) {
258
+ var { isTracingStartedInBrowser, isFunctionCall, isRundownScript, isProcessName } = import__3.Trace.Types.Events;
259
+ function fixupElectronTraceEvents(events) {
260
+ const hasFrameData = events.some(
261
+ (e) => isTracingStartedInBrowser(e) && e.args.data?.frames?.length
262
+ );
263
+ if (hasFrameData) {
264
+ return events;
265
+ }
266
+ const rundownCats = /* @__PURE__ */ new Set([
267
+ "disabled-by-default-devtools.v8-source-rundown",
268
+ "disabled-by-default-devtools.v8-source-rundown-sources"
269
+ ]);
270
+ const isolateToFrame = /* @__PURE__ */ new Map();
271
+ const frameToPid = /* @__PURE__ */ new Map();
272
+ for (const event of events) {
273
+ if (!isFunctionCall(event)) continue;
274
+ const data = event.args?.data;
275
+ if (!data?.isolate || !data?.frame) continue;
276
+ isolateToFrame.set(String(data.isolate), data.frame);
277
+ frameToPid.set(data.frame, event.pid);
278
+ }
279
+ const frameToUrl = /* @__PURE__ */ new Map();
280
+ for (const event of events) {
281
+ if (!isRundownScript(event)) continue;
282
+ const data = event.args.data;
283
+ const url = data.url ?? "";
284
+ const isolate = String(data.isolate ?? "");
285
+ const frame = isolateToFrame.get(isolate);
286
+ if (!frame || !url || !url.startsWith("http")) continue;
287
+ const existing = frameToUrl.get(frame);
288
+ const isDocument = !url.match(/\.(js|cjs|mjs|wasm)(\?|$)/i);
289
+ const existingIsDocument = existing && !existing.match(/\.(js|cjs|mjs|wasm)(\?|$)/i);
290
+ if (!existing || isDocument && !existingIsDocument) {
291
+ frameToUrl.set(frame, url);
292
+ }
293
+ }
294
+ const browserPid = events.find(
295
+ (e) => isProcessName(e) && e.args?.name === "Browser"
296
+ )?.pid;
297
+ const syntheticEvents = [];
298
+ for (const [frame, pid] of frameToPid) {
299
+ const url = frameToUrl.get(frame) ?? "";
300
+ syntheticEvents.push({
301
+ cat: "disabled-by-default-devtools.timeline",
302
+ name: "FrameCommittedInBrowser",
303
+ ph: import__3.Trace.Types.Events.Phase.INSTANT,
304
+ pid: browserPid ?? pid,
305
+ tid: 0,
306
+ ts: 0,
307
+ s: import__3.Trace.Types.Events.Scope.GLOBAL,
308
+ args: {
309
+ data: {
310
+ frame,
311
+ name: "",
312
+ processId: pid,
313
+ url
314
+ }
315
+ }
316
+ });
317
+ }
318
+ const regular = [];
319
+ const rundown = [];
320
+ for (const event of events) {
321
+ if (rundownCats.has(event.cat)) {
322
+ rundown.push(event);
323
+ } else {
324
+ regular.push(event);
325
+ }
326
+ }
327
+ return [...syntheticEvents, ...regular, ...rundown];
328
+ }
329
+ async function verboseFetch(url, headers) {
257
330
  console.log(`[sourcemap] fetching ${url}`);
258
- const response = await fetch(url);
331
+ const response = await fetch(url, headers ? { headers } : void 0);
259
332
  if (!response.ok) {
260
333
  console.warn(
261
334
  `[sourcemap] ${url} -> ${response.status} ${response.statusText}`
@@ -323,11 +396,28 @@ function printUsage() {
323
396
  console.log("\nCommands:");
324
397
  console.log(" selector-stats Top CSS selectors from selector stats and invalidation tracking");
325
398
  console.log(" inp Extract INP (Interaction to Next Paint) breakdown");
326
- console.log(" sourcemap Symbolicate a trace using source maps");
399
+ console.log(' sourcemap Symbolicate a trace using source maps [-H "Header: value"]');
327
400
  console.log(" stats Generate timeline category statistics");
328
401
  }
402
+ function parseHeaders(args) {
403
+ const headers = {};
404
+ for (let i = 0; i < args.length; i++) {
405
+ if (args[i] === "-H" && i + 1 < args.length) {
406
+ const value = args[++i];
407
+ const colonIdx = value.indexOf(":");
408
+ if (colonIdx === -1) {
409
+ console.error(`Invalid header (missing ':'): ${value}`);
410
+ process.exit(1);
411
+ }
412
+ headers[value.slice(0, colonIdx).trim()] = value.slice(colonIdx + 1).trim();
413
+ }
414
+ }
415
+ return headers;
416
+ }
329
417
  async function main() {
330
- const [command, tracePath] = process.argv.slice(2);
418
+ const args = process.argv.slice(2);
419
+ const command = args[0];
420
+ const tracePath = args[1];
331
421
  if (!command || command === "--help" || command === "-h") {
332
422
  printUsage();
333
423
  process.exit(0);
@@ -343,6 +433,12 @@ async function main() {
343
433
  console.error(`Usage: devtools-tracing ${command} <trace-file>`);
344
434
  process.exit(1);
345
435
  }
346
- await run5(tracePath);
436
+ if (command === "sourcemap") {
437
+ const headers = parseHeaders(args.slice(2));
438
+ const options = Object.keys(headers).length > 0 ? { headers } : {};
439
+ await run3(tracePath, options);
440
+ } else {
441
+ await run5(tracePath);
442
+ }
347
443
  }
348
444
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devtools-tracing",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Utilities for working with trace files",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",