2g 0.3.1 → 0.4.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.
@@ -8,13 +8,15 @@ var s = require("./pretty-chunk.js");
8
8
 
9
9
  var o = require("node:fs");
10
10
 
11
- var r = require("./tap-chunk.js");
11
+ var r = require("./api-chunk.js");
12
12
 
13
13
  var i = require("node:readline");
14
14
 
15
15
  var a = require("node:crypto");
16
16
 
17
- var l = "undefined" != typeof document ? document.currentScript : null;
17
+ var l = require("node:child_process");
18
+
19
+ var c = "undefined" != typeof document ? document.currentScript : null;
18
20
 
19
21
  function runCleanCli(e = []) {
20
22
  const o = function parseCleanOptions(e) {
@@ -48,7 +50,7 @@ function runCleanCli(e = []) {
48
50
  })}\n` : `Removed ${r} ${o.all ? "exited" : "stale"} sessions.\n`);
49
51
  }
50
52
 
51
- const c = {
53
+ const p = {
52
54
  since: {
53
55
  type: "string"
54
56
  },
@@ -67,6 +69,13 @@ const c = {
67
69
  }
68
70
  };
69
71
 
72
+ async function warnOnRotationLoss(e, t) {
73
+ if (!await r.detectRotationLoss(e)) {
74
+ return;
75
+ }
76
+ console.warn(`⚠ Earlier events were rotated out of the retained window.\n For a complete trace, start \`${t}\` before the workload.`);
77
+ }
78
+
70
79
  async function* readJsonlStream(e) {
71
80
  const t = i.createInterface({
72
81
  input: e
@@ -84,6 +93,21 @@ async function* readJsonlStream(e) {
84
93
  }
85
94
  }
86
95
 
96
+ async function* filterEvents(e, t) {
97
+ const s = n.compileEventFilter(t.filter);
98
+ const o = n.parseSince(t.since);
99
+ for await (const r of e) {
100
+ if (n.matchesTapOptions(r, {
101
+ filter: t.filter,
102
+ since: o,
103
+ spans: t.spans,
104
+ debug: t.debug
105
+ }, s)) {
106
+ yield r;
107
+ }
108
+ }
109
+ }
110
+
87
111
  function parseSharedTapOptions(e) {
88
112
  return {
89
113
  since: e.since,
@@ -108,7 +132,15 @@ function parseHelp(e) {
108
132
  }).values.help;
109
133
  }
110
134
 
111
- const p = [ ":started", ":done", ":failed" ];
135
+ const u = [ ":started", ":done", ":failed" ];
136
+
137
+ async function convertToChromeTrace(e, t = {}) {
138
+ const n = new TraceConverter(t);
139
+ for await (const t of e) {
140
+ n.add(t);
141
+ }
142
+ return n.toTraceFile();
143
+ }
112
144
 
113
145
  class TraceConverter {
114
146
  #e=[];
@@ -297,7 +329,7 @@ function assignLanes(e) {
297
329
  }
298
330
 
299
331
  function stripSuffix$1(e) {
300
- for (const t of p) {
332
+ for (const t of u) {
301
333
  if (e.endsWith(t)) {
302
334
  return e.slice(0, -t.length);
303
335
  }
@@ -305,7 +337,15 @@ function stripSuffix$1(e) {
305
337
  return e;
306
338
  }
307
339
 
308
- const u = [ ":started", ":done", ":failed" ];
340
+ const d = [ ":started", ":done", ":failed" ];
341
+
342
+ async function convertToOpenTelemetry(e, t = {}) {
343
+ const n = new OpenTelemetryConverter(t);
344
+ for await (const t of e) {
345
+ n.add(t);
346
+ }
347
+ return n.toFile();
348
+ }
309
349
 
310
350
  class OpenTelemetryConverter {
311
351
  #i=a.randomBytes(16).toString("hex");
@@ -491,7 +531,7 @@ function splitEventName(e) {
491
531
  }
492
532
 
493
533
  function stripSuffix(e) {
494
- for (const t of u) {
534
+ for (const t of d) {
495
535
  if (e.endsWith(t)) {
496
536
  return e.slice(0, -t.length);
497
537
  }
@@ -502,7 +542,7 @@ function stripSuffix(e) {
502
542
  async function runExportCli(e) {
503
543
  if (parseHelp(e)) {
504
544
  !function printExportHelp() {
505
- process.stdout.write([ "Usage: 2g export [selector]", "", "Exports session events to Chrome Trace or OTLP JSON.", "", "Options:", " selector Substring of PID, CWD, or command; a running", " session wins ties", " --input <file> Read JSONL events from a file", " -o, --output <file> Write output to a file", " --format <format> chrome-trace or opentelemetry", " --json Print JSON output", " --since <time> Duration ago (5m, 90s), Unix time, or ISO date", " --filter <pattern> Event name prefix (whole segments), e.g. category,", " category:kind; * wildcards; comma-separated", " --spans Keep only span events (log.span() pairs with a _d", " duration); omit to include point events too. A", ' plain "ms" payload field is not a span', " --debug Include debug events (only recorded when the", " command ran with LOG_DEBUG set)", " --tail Follow live events after replaying history", " --timeout <duration> Stop tailing after an absolute duration", " Tail also stops after 30s without new events", "" ].join("\n"));
545
+ process.stdout.write([ "Usage: 2g export [selector]", "", "Exports session events to Chrome Trace or OTLP JSON.", "(Without --tail it reads only the retained window. Rotated out logs may be lost)", "", "--tail attaches to an already-running session and follows live from that point.", " Run it after the session is up and leave it attached while the workload runs.", " It will automatically stop on 30s idle.", "To trace a full command instead, use `2g record`.", "", "Options:", " selector Substring of PID, CWD, or command; a running", " session wins ties", " --input <file> Read JSONL events from a file", " -o, --output <file> Write output to a file", " --format <format> chrome-trace or opentelemetry", " --json Print JSON output", " --since <time> Duration ago (5m, 90s), Unix time, or ISO date", " --filter <pattern> Event name prefix (whole segments), e.g. category,", " category:kind; * wildcards; comma-separated", " --spans Keep only span events (log.span() pairs with a _d", " duration); omit to include point events too. A", ' plain "ms" payload field is not a span', " --debug Include debug events (only recorded when the", " command ran with LOG_DEBUG set)", " --tail Follow live events after replaying history; stops", " automatically on 30s idle", " --timeout <duration> Also stop tailing after this absolute duration", "" ].join("\n"));
506
546
  }();
507
547
  return;
508
548
  }
@@ -511,7 +551,7 @@ async function runExportCli(e) {
511
551
  args: e,
512
552
  allowPositionals: !0,
513
553
  options: {
514
- ...c,
554
+ ...p,
515
555
  input: {
516
556
  type: "string"
517
557
  },
@@ -546,7 +586,7 @@ async function runExportCli(e) {
546
586
  return t;
547
587
  }(s.values.timeout);
548
588
  const i = s.values.format ?? "chrome-trace";
549
- if (!function isExportFormat(e) {
589
+ if (!function isExportFormat$1(e) {
550
590
  return "chrome-trace" === e || "opentelemetry" === e;
551
591
  }(i)) {
552
592
  throw new Error(`Unsupported format: ${i}`);
@@ -577,13 +617,14 @@ async function runExportCli(e) {
577
617
  }
578
618
  let a;
579
619
  let l;
580
- let p;
620
+ let c;
581
621
  if (i.selector) {
582
622
  const e = await r.resolveSession(i.selector);
583
623
  l = function formatSessionProcessName(e) {
584
624
  return e.version ? `${e.command} (v${e.version}, PID ${e.pid})` : `${e.command} (PID ${e.pid})`;
585
625
  }(e);
586
- p = e.pid;
626
+ c = e.pid;
627
+ await warnOnRotationLoss(e.sessionDir, "2g export --tail");
587
628
  a = r.tap(e.sessionDir, {
588
629
  follow: i.follow,
589
630
  debug: i.debug,
@@ -601,37 +642,12 @@ async function runExportCli(e) {
601
642
  yield* readJsonlStream(process.stdin);
602
643
  }();
603
644
  }
604
- const u = async function* filterEvents(e, t) {
605
- const s = n.compileEventFilter(t.filter);
606
- const o = n.parseSince(t.since);
607
- for await (const r of e) {
608
- if (n.matchesTapOptions(r, {
609
- filter: t.filter,
610
- since: o,
611
- spans: t.spans,
612
- debug: t.debug
613
- }, s)) {
614
- yield r;
615
- }
616
- }
617
- }(a, i);
618
- const d = "opentelemetry" === i.format ? await async function convertToOpenTelemetry(e, t = {}) {
619
- const n = new OpenTelemetryConverter(t);
620
- for await (const t of e) {
621
- n.add(t);
622
- }
623
- return n.toFile();
624
- }(u, {
625
- pid: p,
645
+ const u = filterEvents(a, i);
646
+ const d = "opentelemetry" === i.format ? await convertToOpenTelemetry(u, {
647
+ pid: c,
626
648
  processName: l
627
- }) : await async function convertToChromeTrace(e, t = {}) {
628
- const n = new TraceConverter(t);
629
- for await (const t of e) {
630
- n.add(t);
631
- }
632
- return n.toTraceFile();
633
- }(u, {
634
- pid: p,
649
+ }) : await convertToChromeTrace(u, {
650
+ pid: c,
635
651
  processName: l
636
652
  });
637
653
  const f = JSON.stringify(d, null, 2);
@@ -646,6 +662,99 @@ function escapeTsv(e) {
646
662
  return String(e).replace(/\\/g, "\\\\").replace(/\t/g, "\\t").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
647
663
  }
648
664
 
665
+ async function runRecordCli(e) {
666
+ if (parseHelp(e)) {
667
+ !function printRecordHelp() {
668
+ process.stdout.write([ "Usage: 2g record [options] -- <command> [args...]", "", "Runs <command>, traces the events it emits, and writes the trace on exit.", "", "This is the reliable way to capture a one-off run (a build, a script): 2g", "owns the process, so the trace is complete end-to-end with no window to", "attach within and nothing to rotate out. To trace an already-running server", "instead, use `2g export <selector> --tail`.", "", "Options:", " -o, --output <file> Write the trace to a file (default: stdout)", " --format <format> chrome-trace or opentelemetry", " --json Print JSON output", " --filter <pattern> Event name prefix (whole segments), e.g. category,", " category:kind; * wildcards; comma-separated", " --spans Keep only span events (log.span() pairs with a _d", " duration); omit to include point events too", " --debug Enable and export debug events; no LOG_DEBUG needed", "", "Example:", " 2g record -o trace.json -- expo export # run & trace a build end-to-end", "" ].join("\n"));
669
+ }();
670
+ return;
671
+ }
672
+ const n = e.indexOf("--");
673
+ const i = -1 === n ? e : e.slice(0, n);
674
+ const a = -1 === n ? [] : e.slice(n + 1);
675
+ const c = function parseRecordOptions(e) {
676
+ const n = t.parseArgs({
677
+ args: e,
678
+ allowPositionals: !0,
679
+ options: {
680
+ filter: {
681
+ type: "string",
682
+ multiple: !0
683
+ },
684
+ spans: {
685
+ type: "boolean"
686
+ },
687
+ debug: {
688
+ type: "boolean"
689
+ },
690
+ output: {
691
+ type: "string",
692
+ short: "o"
693
+ },
694
+ format: {
695
+ type: "string"
696
+ },
697
+ json: {
698
+ type: "boolean"
699
+ },
700
+ help: {
701
+ type: "boolean",
702
+ short: "h"
703
+ }
704
+ }
705
+ });
706
+ const s = n.values.format ?? "chrome-trace";
707
+ if (!function isExportFormat(e) {
708
+ return "chrome-trace" === e || "opentelemetry" === e;
709
+ }(s)) {
710
+ throw new Error(`Unsupported format: ${s}`);
711
+ }
712
+ return {
713
+ output: n.values.output,
714
+ format: s,
715
+ filter: n.values.filter?.flatMap(e => e.split(",")).map(e => e.trim()).filter(Boolean),
716
+ spans: !0 === n.values.spans,
717
+ debug: !0 === n.values.debug,
718
+ json: n.values.json ?? !1
719
+ };
720
+ }(i);
721
+ if (!a.length) {
722
+ throw new Error("record needs a command to run: 2g record [options] -- <command> [args...]");
723
+ }
724
+ if (c.json) {
725
+ s.redirectConsoleToStderr();
726
+ }
727
+ const p = r.captureEvents({
728
+ debug: c.debug,
729
+ filter: c.filter
730
+ });
731
+ const u = l.spawn(a[0], a.slice(1), p.spawnOptions({
732
+ env: process.env,
733
+ stdio: [ "inherit", c.output ? "inherit" : 2, "inherit" ]
734
+ }));
735
+ p.attach(u);
736
+ const d = new Promise((e, t) => {
737
+ u.on("exit", t => e(t ?? 0));
738
+ u.on("error", e => t(new Error(`Failed to run ${a[0]}: ${e.message}`)));
739
+ });
740
+ const f = {
741
+ pid: u.pid,
742
+ processName: a.join(" ")
743
+ };
744
+ const m = filterEvents(p, c);
745
+ const h = "opentelemetry" === c.format ? await convertToOpenTelemetry(m, f) : await convertToChromeTrace(m, f);
746
+ const g = await d;
747
+ const v = JSON.stringify(h, null, 2);
748
+ if (c.output) {
749
+ o.writeFileSync(c.output, `${v}\n`);
750
+ } else {
751
+ process.stdout.write(`${v}\n`);
752
+ }
753
+ if (g) {
754
+ process.exitCode = g;
755
+ }
756
+ }
757
+
649
758
  async function runTapCli(e) {
650
759
  if (parseHelp(e)) {
651
760
  !function printTapHelp() {
@@ -658,7 +767,7 @@ async function runTapCli(e) {
658
767
  args: e,
659
768
  allowPositionals: !0,
660
769
  options: {
661
- ...c,
770
+ ...p,
662
771
  format: {
663
772
  type: "string"
664
773
  },
@@ -690,6 +799,7 @@ async function runTapCli(e) {
690
799
  s.redirectConsoleToStderr();
691
800
  }
692
801
  const o = await r.resolveSession(n.selector);
802
+ await warnOnRotationLoss(o.sessionDir, "2g tap --tail");
693
803
  const i = s.shouldColorizeStream(process.stdout);
694
804
  for await (const e of r.tap(o.sessionDir, n)) {
695
805
  if (n.pretty) {
@@ -705,9 +815,9 @@ async function runTapCli(e) {
705
815
 
706
816
  async function main(e = process.argv.slice(2)) {
707
817
  const [n, ...o] = e;
708
- if (!n || "--help" === n || "-h" === n) {
818
+ if (!n || "--help" === n || "-h" === n || "help" === n) {
709
819
  !function printHelp() {
710
- process.stdout.write([ "Usage: 2g <command>", "", "Reads structured JSONL event logs that instrumented CLIs record during runs", "into the system temp dir, with no setup: run a command, then tap it here.", "", "Commands:", " ps [selector] List recorded sessions", " --active, -a (running only) --json", "", " tap [selector] Replay session events as JSONL, oldest first", " --since <time> --filter <pattern> --spans --debug --format pretty", " --tail (follow live events after replay; runs until killed)", "", " export [selector] Export events to Chrome Trace or OTLP JSON", " --input <jsonl-file> -o, --output <file> --json", " --format chrome-trace|opentelemetry", " --since <time> --filter <pattern> --spans --debug", " --tail --timeout <duration> (absolute; tail also stops after 30s idle)", "", " typegen List every event name/payload a project can emit", " -p, --project <tsconfig> -o, --output <file> --format dts|json --json", "", " clean Remove stale sessions", " --all, -a (remove all exited sessions) --json", "", "Typical use:", " 2g ps -a # what is running now?", ' 2g tap "expo start" --tail # follow live events', ' 2g tap "expo start" --since 5m --format pretty # recent history, readable', ' 2g tap "expo start" --filter metro:bundling # narrow to one area', ' 2g export "expo start" -o trace.json # flame chart of a run', "", "Selectors match sessions by substring of PID, CWD, or command. If several", "match, and exactly one of them is running, it is chosen; otherwise 2g errors", "and lists the candidates.", "", "--filter takes comma-separated event-name prefixes matched on whole", 'segments: "category" and "category:kind" both match "category:kind:test";', "* matches anything. Discover names by tapping unfiltered or via typegen.", "", "--since accepts a duration ago (5m, 90s, 1h30m), Unix time, or ISO date.", '--spans keeps only span events (log.span() pairs with a "_d" duration in', "ms); without it, point events are included alongside spans.", "", 'Events are single JSON lines: {"_e":"category:kind","_t":<epoch ms>,...}.', '"_d" is a span duration, "_w" a worker id; debug events need --debug.', "", "Set LOG_EVENTS=<file|fd> on an instrumented command to redirect its events.", "Debug events are only recorded when the command runs with LOG_DEBUG set.", "", "Pass LOG_DEBUG=<filter> when debugging; it also mirrors matching events to", "stderr. Reading them back needs --debug.", "", "Run 2g <command> --help for per-command help.", "" ].join("\n"));
820
+ process.stdout.write([ "Usage: 2g <command>", "", "Reads structured JSONL event logs that instrumented CLIs record during runs", "into the system temp dir, with no setup: run a command, then tap it here.", "", "Commands:", " ps [selector] List recorded sessions", " --active, -a (running only) --json", "", " tap [selector] Replay session events as JSONL, oldest first", " --since <time> --filter <pattern> --spans --debug --format pretty", " --tail (follow live events after replay; runs until killed)", "", " record -- <command> Run a command, trace it end-to-end, write on exit", " -o, --output <file> --format chrome-trace|opentelemetry --json", " --filter <pattern> --spans --debug", "", " export [selector] Export a past or live (--tail) session", " --input <jsonl-file> -o, --output <file> --json", " --format chrome-trace|opentelemetry", " --since <time> --filter <pattern> --spans --debug", " --tail --timeout <duration> (absolute; tail also stops after 30s idle)", "", " typegen List every event name/payload a project can emit", " -p, --project <tsconfig> -o, --output <file> --format dts|json --json", "", " clean Remove stale sessions", " --all, -a (remove all exited sessions) --json", "", "Typical use:", " 2g ps -a # what is running now?", ' 2g tap "expo start" --tail # follow live events', ' 2g tap "expo start" --since 5m --format pretty # recent history, readable', ' 2g tap "expo start" --filter metro:bundling # narrow to one area', "", "Trace a run to a flame chart:", " 2g record -o trace.json -- expo export # run & trace a build", ' 2g export "expo start" --tail -o trace.json # attach to a running server', ' 2g export "expo start" -o trace.json # after the fact (partial)', "", "Selectors match sessions by substring of PID, CWD, or command. If several", "match, and exactly one of them is running, it is chosen; otherwise 2g errors", "and lists the candidates.", "", "--filter takes comma-separated event-name prefixes matched on whole", 'segments: "category" and "category:kind" both match "category:kind:test";', "* matches anything. Discover names by tapping unfiltered or via typegen.", "", "--since accepts a duration ago (5m, 90s, 1h30m), Unix time, or ISO date.", '--spans keeps only span events (log.span() pairs with a "_d" duration in', "ms); without it, point events are included alongside spans.", "", 'Events are single JSON lines: {"_e":"category:kind","_t":<epoch ms>,...}.', '"_d" is a span duration, "_w" a worker id; debug events need --debug.', "", "Set LOG_EVENTS=<file|fd> on an instrumented command to redirect its events.", "Debug events are only recorded when the command runs with LOG_DEBUG set.", "", "Pass LOG_DEBUG=<filter> when debugging; it also mirrors matching events to", "stderr. Reading them back needs --debug.", "", "Run 2g <command> --help for per-command help.", "" ].join("\n"));
711
821
  }();
712
822
  return 0;
713
823
  }
@@ -777,6 +887,10 @@ async function main(e = process.argv.slice(2)) {
777
887
  runCleanCli(o);
778
888
  return 0;
779
889
 
890
+ case "record":
891
+ await runRecordCli(o);
892
+ return 0;
893
+
780
894
  case "export":
781
895
  await runExportCli(o);
782
896
  return 0;
@@ -788,7 +902,7 @@ async function main(e = process.argv.slice(2)) {
788
902
 
789
903
  if (function isCliEntrypoint() {
790
904
  try {
791
- return process.argv[1] === e.fileURLToPath("undefined" == typeof document ? require("url").pathToFileURL(__filename).href : l && "SCRIPT" === l.tagName.toUpperCase() && l.src || new URL("dist/chunks/cli-chunk.js", document.baseURI).href);
905
+ return process.argv[1] === e.fileURLToPath("undefined" == typeof document ? require("url").pathToFileURL(__filename).href : c && "SCRIPT" === c.tagName.toUpperCase() && c.src || new URL("dist/chunks/cli-chunk.js", document.baseURI).href);
792
906
  } catch {
793
907
  return !1;
794
908
  }