autotel-terminal 17.0.4 → 17.0.5

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/dist/cli.cjs CHANGED
@@ -111,6 +111,25 @@ function createTerminalSpanStream(processor) {
111
111
  const startTime = timeToMs(span.startTime);
112
112
  const endTime = timeToMs(span.endTime);
113
113
  const durationMs = endTime - startTime;
114
+ const resourceAttrs = span.resource?.attributes ?? {};
115
+ const spanAttrs = span.attributes;
116
+ const mergedAttrs = {};
117
+ for (const [k, v] of Object.entries(resourceAttrs)) {
118
+ mergedAttrs[k] = v;
119
+ }
120
+ for (const [k, v] of Object.entries(spanAttrs)) {
121
+ mergedAttrs[k] = v;
122
+ }
123
+ const spanEvents = span.events?.length ? span.events.map((e) => ({
124
+ name: e.name,
125
+ timeMs: timeToMs(e.time),
126
+ attributes: e.attributes
127
+ })) : void 0;
128
+ const spanLinks = span.links?.length ? span.links.map((l) => ({
129
+ traceId: l.context.traceId,
130
+ spanId: l.context.spanId,
131
+ attributes: l.attributes
132
+ })) : void 0;
114
133
  const event = {
115
134
  name: span.name,
116
135
  spanId: spanContext.spanId,
@@ -121,7 +140,9 @@ function createTerminalSpanStream(processor) {
121
140
  durationMs,
122
141
  status: mapStatus(span.status.code),
123
142
  kind: mapKind(span.kind),
124
- attributes: span.attributes
143
+ attributes: mergedAttrs,
144
+ ...spanEvents ? { events: spanEvents } : {},
145
+ ...spanLinks ? { links: spanLinks } : {}
125
146
  };
126
147
  callback(event);
127
148
  });
@@ -561,6 +582,123 @@ function buildErrorSummaries(traceSummaries) {
561
582
  return out;
562
583
  }
563
584
 
585
+ // src/lib/topology-model.ts
586
+ function getServiceName2(span) {
587
+ const attrs = span.attributes ?? {};
588
+ const serviceName = attrs["service.name"] ?? attrs["resource.service.name"];
589
+ return serviceName || "unknown";
590
+ }
591
+ function getPeerService(span) {
592
+ const attrs = span.attributes ?? {};
593
+ const peerService = attrs["peer.service"] ?? attrs["db.system"] ?? attrs["messaging.system"] ?? attrs["http.host"];
594
+ return peerService ?? null;
595
+ }
596
+ function buildServiceGraph(spans) {
597
+ const byService = /* @__PURE__ */ new Map();
598
+ for (const span of spans) {
599
+ const svc = getServiceName2(span);
600
+ const entry = byService.get(svc) ?? {
601
+ durations: [],
602
+ spanCount: 0,
603
+ errorCount: 0
604
+ };
605
+ entry.spanCount += 1;
606
+ entry.durations.push(span.durationMs);
607
+ if (span.status === "ERROR") entry.errorCount += 1;
608
+ byService.set(svc, entry);
609
+ }
610
+ const services = [];
611
+ for (const [serviceName, { durations, spanCount, errorCount }] of byService) {
612
+ if (spanCount === 0) continue;
613
+ const avgDurationMs = durations.reduce((acc, d) => acc + d, 0) / durations.length;
614
+ const sorted = durations.toSorted((a, b) => a - b);
615
+ const p95Index = Math.floor(sorted.length * 0.95);
616
+ const p95DurationMs = sorted[p95Index] ?? sorted.at(-1) ?? 0;
617
+ services.push({
618
+ serviceName,
619
+ spanCount,
620
+ errorCount,
621
+ avgDurationMs,
622
+ p95DurationMs
623
+ });
624
+ }
625
+ const byEdge = /* @__PURE__ */ new Map();
626
+ for (const span of spans) {
627
+ const to = getPeerService(span);
628
+ if (!to) continue;
629
+ const from = getServiceName2(span);
630
+ const key = `${from}\u2192${to}`;
631
+ const entry = byEdge.get(key) ?? { spanCount: 0, errorCount: 0 };
632
+ entry.spanCount += 1;
633
+ if (span.status === "ERROR") entry.errorCount += 1;
634
+ byEdge.set(key, entry);
635
+ }
636
+ const edges = [];
637
+ for (const [key, { spanCount, errorCount }] of byEdge) {
638
+ const [fromService, toService] = key.split("\u2192");
639
+ edges.push({ fromService, toService, spanCount, errorCount });
640
+ }
641
+ services.sort((a, b) => b.spanCount - a.spanCount);
642
+ edges.sort((a, b) => b.spanCount - a.spanCount);
643
+ return { services, edges };
644
+ }
645
+
646
+ // src/lib/topology-render.ts
647
+ function renderTopologyAscii(graph) {
648
+ if (graph.services.length === 0) {
649
+ return [" No services detected yet."];
650
+ }
651
+ const targetServices = new Set(graph.edges.map((e) => e.toService));
652
+ const roots = graph.services.filter(
653
+ (s) => !targetServices.has(s.serviceName)
654
+ );
655
+ const rootList = roots.length > 0 ? roots : [...graph.services];
656
+ const lines = [];
657
+ for (const root of rootList) {
658
+ renderNode(graph, root.serviceName, "", true, lines, /* @__PURE__ */ new Set());
659
+ }
660
+ return lines;
661
+ }
662
+ function renderNode(graph, serviceName, prefix, isRoot, lines, ancestors) {
663
+ const svc = graph.services.find((s) => s.serviceName === serviceName);
664
+ const label = svc ? formatServiceLine(svc) : `[${serviceName}]`;
665
+ if (isRoot) {
666
+ lines.push(label);
667
+ }
668
+ if (ancestors.has(serviceName)) return;
669
+ const pathAncestors = new Set(ancestors);
670
+ pathAncestors.add(serviceName);
671
+ const outgoing = graph.edges.filter((e) => e.fromService === serviceName);
672
+ for (let i = 0; i < outgoing.length; i++) {
673
+ const edge = outgoing[i];
674
+ const isLast = i === outgoing.length - 1;
675
+ const connector = isLast ? "\u2514" : "\u251C";
676
+ const childPrefix = isLast ? " " : "\u2502 ";
677
+ const downstream = graph.services.find(
678
+ (s) => s.serviceName === edge.toService
679
+ );
680
+ const edgeLabel = formatEdgeLabel(edge);
681
+ const downstreamLabel = downstream ? formatServiceLine(downstream) : `[${edge.toService}]`;
682
+ lines.push(`${prefix} ${connector}\u2500\u2500${edgeLabel}\u2500\u2500\u2192 ${downstreamLabel}`);
683
+ renderNode(
684
+ graph,
685
+ edge.toService,
686
+ prefix + " " + childPrefix,
687
+ false,
688
+ lines,
689
+ pathAncestors
690
+ );
691
+ }
692
+ }
693
+ function formatServiceLine(svc) {
694
+ const errPart = svc.errorCount > 0 ? ` \xB7 ${svc.errorCount} err` : "";
695
+ return `[${svc.serviceName}] ${svc.spanCount} spans${errPart} \xB7 p95 ${formatDurationMs(svc.p95DurationMs)}`;
696
+ }
697
+ function formatEdgeLabel(edge) {
698
+ const errPart = edge.errorCount > 0 ? `, ${edge.errorCount} err` : "";
699
+ return `(${edge.spanCount}${errPart})`;
700
+ }
701
+
564
702
  // src/lib/export-model.ts
565
703
  function exportTraceToJson(trace, logs) {
566
704
  const exported = {
@@ -1078,6 +1216,8 @@ function Dashboard({
1078
1216
  [spansForSelectedService]
1079
1217
  );
1080
1218
  const selectedTraceSummary = drilldownTraceId == null ? filteredSummaries[selected] ?? null : filteredSummaries.find((t2) => t2.traceId === drilldownTraceId) ?? null;
1219
+ const serviceGraph = react.useMemo(() => buildServiceGraph(spans), [spans]);
1220
+ const topologyLines = react.useMemo(() => renderTopologyAscii(serviceGraph), [serviceGraph]);
1081
1221
  const errorSummaries = react.useMemo(
1082
1222
  () => buildErrorSummaries(traceSummaries),
1083
1223
  [traceSummaries]
@@ -1484,6 +1624,13 @@ Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.len
1484
1624
  setDrilldownSelectedIndex(0);
1485
1625
  setDrilldownScrollOffset(0);
1486
1626
  }
1627
+ if (input === "G") {
1628
+ setViewMode((m) => m === "topology" ? "trace" : "topology");
1629
+ setSelected(0);
1630
+ setDrilldownTraceId(null);
1631
+ setDrilldownSelectedIndex(0);
1632
+ setDrilldownScrollOffset(0);
1633
+ }
1487
1634
  if (input === "c") {
1488
1635
  setSpans([]);
1489
1636
  setLogs([]);
@@ -1625,7 +1772,7 @@ ${json}
1625
1772
  { isActive: isRawModeSupported }
1626
1773
  );
1627
1774
  const headerRight = recording ? "[Recording]" : paused ? "[Paused]" : "[Live]";
1628
- const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : "errors";
1775
+ const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : viewMode === "topology" ? "topology" : "errors";
1629
1776
  const showNewError = newErrorCount > 0;
1630
1777
  function renderTreeRow(node, index) {
1631
1778
  const isSel = drilldownTraceId != null && index === drilldownSelectedIndex;
@@ -1639,8 +1786,9 @@ ${json}
1639
1786
  flexDirection: "row",
1640
1787
  children: [
1641
1788
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { backgroundColor: isSel ? "blue" : void 0, color: isSel ? "white" : void 0, children: isSel ? "\u25B8 " : " " }),
1789
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: node.span.status === "ERROR" ? "red" : void 0, children: node.span.status === "ERROR" ? "\u2717" : " " }),
1642
1790
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: prefix }),
1643
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(node.span.name, 24) }),
1791
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(node.span.name, 23) }),
1644
1792
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: svcColor, children: [
1645
1793
  " ",
1646
1794
  truncate(svcName, 10)
@@ -1706,12 +1854,20 @@ ${json}
1706
1854
  ] })
1707
1855
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", justifyContent: "space-between", children: [
1708
1856
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: drilldownTraceId == null ? "\u2191/\u2193 select \u2022 Enter open \u2022 Tab cycle tabs \u2022 Esc back \u2022 T trace \u2022 L logs \u2022 a AI \u2022 ? help" : "\u2191/\u2193 select \u2022 Tab cycle tabs \u2022 Esc back \u2022 T trace \u2022 L logs \u2022 a AI \u2022 ? help" }),
1709
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: viewMode === "trace" ? `traces ${filteredSummaries.length}/${traceSummaries.length}` : viewMode === "span" ? `spans ${filteredSpans.length}/${spans.length}` : viewMode === "service-summary" ? `services ${serviceStats.length}` : viewMode === "errors" ? `errors ${filteredErrorSummaries.length}/${errorSummaries.length}` : `logs ${filteredLogs.length}/${logs.length}` })
1857
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: viewMode === "trace" ? `traces ${filteredSummaries.length}/${traceSummaries.length}` : viewMode === "span" ? `spans ${filteredSpans.length}/${spans.length}` : viewMode === "service-summary" ? `services ${serviceStats.length}` : viewMode === "errors" ? `errors ${filteredErrorSummaries.length}/${errorSummaries.length}` : viewMode === "topology" ? `services ${serviceGraph.services.length} \xB7 edges ${serviceGraph.edges.length}` : `logs ${filteredLogs.length}/${logs.length}` })
1710
1858
  ] }),
1711
- showHelp && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Views: t/l/v/E \u2022 Search: / \u2022 Filters: e/S/R/H/f/x \u2022 Capture: p/r/J \u2022 AI: a \u2022 Clear: c" })
1859
+ showHelp && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Views: t/l/v/E/G \u2022 Search: / \u2022 Filters: e/S/R/H/f/x \u2022 Capture: p/r/J \u2022 AI: a \u2022 Clear: c" })
1712
1860
  ] }),
1713
1861
  /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 0, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: spanFilters.serviceName || spanFilters.route || spanFilters.statusGroup !== "all" || spanFilters.traceId ? `filters:${spanFilters.serviceName ? ` service=${spanFilters.serviceName}` : ""}${spanFilters.route ? ` route=${spanFilters.route}` : ""}${spanFilters.statusGroup && spanFilters.statusGroup !== "all" ? ` status=${spanFilters.statusGroup}` : ""}${spanFilters.traceId ? ` trace=${spanFilters.traceId.slice(0, 8)}\u2026` : ""}` : "filters: none" }) }),
1714
- drilldownTraceId != null ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, children: [
1862
+ viewMode === "topology" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
1863
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Service Topology" }),
1864
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press G to toggle \xB7 Shows service dependencies from span data" }),
1865
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", marginTop: 1, children: topologyLines.map((line, i) => {
1866
+ const hasErr = line.includes(" err");
1867
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: hasErr ? "red" : void 0, children: line }, `topo-${i}`);
1868
+ }) })
1869
+ ] }),
1870
+ viewMode !== "topology" && (drilldownTraceId != null ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, children: [
1715
1871
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "column", children: [
1716
1872
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, children: [
1717
1873
  "Trace ",
@@ -1810,7 +1966,8 @@ ${json}
1810
1966
  const svcColor = getServiceColor(svcName);
1811
1967
  const kindStr = (s.kind ?? "").padEnd(KIND_COL);
1812
1968
  const svcStr = truncate(svcName, SERVICE_COL - 2).padEnd(SERVICE_COL);
1813
- const namePart = `${indent}${truncate(s.name, nameWidth)}`.padEnd(NAME_COL);
1969
+ const errorMark = s.status === "ERROR" ? "\u2717" : " ";
1970
+ const namePart = `${indent}${truncate(s.name, nameWidth - 1)}`.padEnd(NAME_COL - 1);
1814
1971
  const bar = buildWaterfallBar(
1815
1972
  s.startTime,
1816
1973
  s.durationMs,
@@ -1821,6 +1978,7 @@ ${json}
1821
1978
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
1822
1979
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { backgroundColor: isSel ? "blue" : void 0, color: isSel ? "white" : void 0, children: [
1823
1980
  isSel ? "\u25B8" : " ",
1981
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: s.status === "ERROR" ? "red" : void 0, children: errorMark }),
1824
1982
  namePart
1825
1983
  ] }),
1826
1984
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: svcColor, children: [
@@ -1927,6 +2085,53 @@ ${json}
1927
2085
  ": ",
1928
2086
  truncate(String(v), 28)
1929
2087
  ] }, k))
2088
+ ] }),
2089
+ span.events && span.events.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginTop: 0, children: [
2090
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, dimColor: true, children: [
2091
+ "Events (",
2092
+ span.events.length,
2093
+ ")"
2094
+ ] }),
2095
+ span.events.slice(0, 5).map((ev, i) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
2096
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: ev.name === "exception" ? "red" : "yellow", children: [
2097
+ " ",
2098
+ "\u25C6",
2099
+ " ",
2100
+ truncate(ev.name, 20)
2101
+ ] }),
2102
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2103
+ " ",
2104
+ "+",
2105
+ formatDurationMs(ev.timeMs - span.startTime)
2106
+ ] }),
2107
+ ev.attributes && Object.keys(ev.attributes).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2108
+ " ",
2109
+ Object.entries(ev.attributes).slice(0, 2).map(([k, v]) => `${k}=${String(v)}`).join(" ")
2110
+ ] })
2111
+ ] }, `ev-${i}`))
2112
+ ] }),
2113
+ span.links && span.links.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginTop: 0, children: [
2114
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, dimColor: true, children: [
2115
+ "Links (",
2116
+ span.links.length,
2117
+ ")"
2118
+ ] }),
2119
+ span.links.slice(0, 5).map((lnk, i) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
2120
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [
2121
+ " ",
2122
+ "\u2192",
2123
+ " trace:",
2124
+ lnk.traceId.slice(0, 8),
2125
+ "\u2026",
2126
+ " span:",
2127
+ lnk.spanId.slice(0, 8),
2128
+ "\u2026"
2129
+ ] }),
2130
+ lnk.attributes && Object.keys(lnk.attributes).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2131
+ " ",
2132
+ Object.entries(lnk.attributes).slice(0, 2).map(([k, v]) => `${k}=${String(v)}`).join(" ")
2133
+ ] })
2134
+ ] }, `lnk-${i}`))
1930
2135
  ] })
1931
2136
  ] });
1932
2137
  })(),
@@ -2407,7 +2612,7 @@ ${json}
2407
2612
  ] })
2408
2613
  }
2409
2614
  )
2410
- ] }),
2615
+ ] })),
2411
2616
  showStats && /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2412
2617
  "Spans: ",
2413
2618
  stats.total,
@@ -2603,6 +2808,8 @@ function* extractSpans(payload) {
2603
2808
  if (!Array.isArray(resourceSpans)) return;
2604
2809
  for (const resourceSpan of resourceSpans) {
2605
2810
  if (!resourceSpan || typeof resourceSpan !== "object") continue;
2811
+ const resource = resourceSpan.resource;
2812
+ const resourceAttrs = attrsToRecord(resource?.attributes);
2606
2813
  const scopeSpans = resourceSpan.scopeSpans;
2607
2814
  if (!Array.isArray(scopeSpans)) continue;
2608
2815
  for (const scopeSpan of scopeSpans) {
@@ -2611,15 +2818,33 @@ function* extractSpans(payload) {
2611
2818
  if (!Array.isArray(spans)) continue;
2612
2819
  for (const span of spans) {
2613
2820
  if (span && typeof span === "object") {
2614
- yield span;
2821
+ yield { span, resourceAttrs };
2615
2822
  }
2616
2823
  }
2617
2824
  }
2618
2825
  }
2619
2826
  }
2620
- function otlpSpanToTerminalEvent(span) {
2827
+ function otlpSpanToTerminalEvent(span, resourceAttrs = {}) {
2621
2828
  const startTime = toMs(span.startTimeUnixNano);
2622
2829
  const endTime = toMs(span.endTimeUnixNano);
2830
+ const spanAttrs = attrsToRecord(span.attributes);
2831
+ const mergedAttrs = {};
2832
+ for (const [k, v] of Object.entries(resourceAttrs)) {
2833
+ mergedAttrs[k] = v;
2834
+ }
2835
+ for (const [k, v] of Object.entries(spanAttrs)) {
2836
+ mergedAttrs[k] = v;
2837
+ }
2838
+ const parsedEvents = span.events?.length ? span.events.map((e) => ({
2839
+ name: e.name || "",
2840
+ timeMs: toMs(e.timeUnixNano),
2841
+ attributes: attrsToRecord(e.attributes)
2842
+ })) : void 0;
2843
+ const parsedLinks = span.links?.length ? span.links.map((l) => ({
2844
+ traceId: normalizeHexId(l.traceId, 32),
2845
+ spanId: normalizeHexId(l.spanId, 16),
2846
+ attributes: attrsToRecord(l.attributes)
2847
+ })) : void 0;
2623
2848
  return {
2624
2849
  name: span.name || "unnamed",
2625
2850
  spanId: normalizeHexId(span.spanId, 16),
@@ -2630,7 +2855,9 @@ function otlpSpanToTerminalEvent(span) {
2630
2855
  durationMs: Math.max(0, endTime - startTime),
2631
2856
  status: mapStatus2(span.status?.code),
2632
2857
  kind: mapKind2(span.kind),
2633
- attributes: attrsToRecord(span.attributes)
2858
+ attributes: mergedAttrs,
2859
+ ...parsedEvents ? { events: parsedEvents } : {},
2860
+ ...parsedLinks ? { links: parsedLinks } : {}
2634
2861
  };
2635
2862
  }
2636
2863
  async function readJsonBody(req) {
@@ -2655,8 +2882,8 @@ function sendJson(res, status, data) {
2655
2882
  }
2656
2883
  function parseOtlpEvents(payload) {
2657
2884
  const events = [];
2658
- for (const span of extractSpans(payload)) {
2659
- events.push(otlpSpanToTerminalEvent(span));
2885
+ for (const { span, resourceAttrs } of extractSpans(payload)) {
2886
+ events.push(otlpSpanToTerminalEvent(span, resourceAttrs));
2660
2887
  }
2661
2888
  return events;
2662
2889
  }