rails-profiler 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59da55aba7ca1284fdda2c948c456bffc05de245d876dba9c9b477257f56654d
4
- data.tar.gz: e3df3028c4a547e83cda3cb2d6170c279c059d140725efb38a489022e02d15e9
3
+ metadata.gz: 84a94ffe07e5c055045a375cc92f69638618f23a169717b532cadcecc98ac95f
4
+ data.tar.gz: c756cfb2bd7eff713976aaadc7e0d19375fc6747b47c6c421753dec3cc4bccdd
5
5
  SHA512:
6
- metadata.gz: f04910c4c4592f4ee1168c5010330d36d460deb8b7ef890ecc4680013c107aaba9c627cbdeb5674a401379b457e0ea13077dcafb5490ca2124804be152fadeb7
7
- data.tar.gz: d50f9dcded83688a09009c5aa5436e7726007b7b72e44b78d4107d11ddbd72046eef1e812bf606804cb1965fd58ccdae999b7542651baf0a5ddd280c7b2812a4
6
+ metadata.gz: e971cfd7febeafd91af85f6dbf922b0d092f76bb3c77223b9d00d1b682af6c33a4e1fa50905d426dd9326b8e1d13e22e09da3c9c32e05a194c9d9e539969a206
7
+ data.tar.gz: f1a8b324dcae79a81bc63fa8a319f815c0610dbaa606c5db1c41c4d5226a69812a4946cb85987aa7e7a452a098478d3f61c61b17178b2dc396d745c7e77b26f1
@@ -484,6 +484,10 @@
484
484
  const headers = requestData.headers;
485
485
  const responseBody = requestData.response_body;
486
486
  const responseBodyEncoding = requestData.response_body_encoding;
487
+ const controllerAction = requestData.controller_action;
488
+ const routeName = requestData.route_name;
489
+ const routePattern = requestData.route_pattern;
490
+ const routeParams = requestData.route_params;
487
491
  return /* @__PURE__ */ u3(k, { children: [
488
492
  /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-header", children: [
489
493
  "Request & Response",
@@ -503,6 +507,22 @@
503
507
  /* @__PURE__ */ u3("span", { children: "Status" }),
504
508
  /* @__PURE__ */ u3("strong", { class: cls, children: profile.status })
505
509
  ] }),
510
+ controllerAction && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
511
+ /* @__PURE__ */ u3("span", { children: "Controller#Action" }),
512
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--xs profiler-text--mono", children: controllerAction })
513
+ ] }),
514
+ routeName && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
515
+ /* @__PURE__ */ u3("span", { children: "Route Name" }),
516
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--xs profiler-text--mono", children: routeName })
517
+ ] }),
518
+ routePattern && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
519
+ /* @__PURE__ */ u3("span", { children: "Route Pattern" }),
520
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--xs profiler-text--mono", children: routePattern })
521
+ ] }),
522
+ routeParams && Object.keys(routeParams).length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
523
+ /* @__PURE__ */ u3("span", { children: "Route Params" }),
524
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--xs profiler-text--mono", children: Object.entries(routeParams).map(([k3, v3]) => `${k3}: ${v3}`).join(", ") })
525
+ ] }),
506
526
  profile.params && Object.keys(profile.params).length > 0 && /* @__PURE__ */ u3(k, { children: [
507
527
  /* @__PURE__ */ u3("div", { class: "profiler-section__header profiler-mt-3", children: "Parameters" }),
508
528
  Object.entries(profile.params).map(([key, value]) => /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
@@ -894,6 +914,38 @@
894
914
  ] });
895
915
  }
896
916
 
917
+ // app/assets/typescript/profiler/components/toolbar/panels/RoutesPanel.tsx
918
+ function RoutesPanel({ routesData }) {
919
+ const { total, matched } = routesData;
920
+ return /* @__PURE__ */ u3(k, { children: [
921
+ /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-header", children: [
922
+ "Routes",
923
+ /* @__PURE__ */ u3("span", { class: "profiler-float-right", children: [
924
+ total,
925
+ " routes"
926
+ ] })
927
+ ] }),
928
+ /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-content", children: matched ? /* @__PURE__ */ u3(k, { children: [
929
+ /* @__PURE__ */ u3("div", { class: "profiler-section__header", children: "Matched Route" }),
930
+ /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
931
+ /* @__PURE__ */ u3("span", { children: "Pattern" }),
932
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--xs profiler-text--mono", children: matched.pattern })
933
+ ] }),
934
+ matched.name && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
935
+ /* @__PURE__ */ u3("span", { children: "Name" }),
936
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--xs profiler-text--mono", children: [
937
+ matched.name,
938
+ "_path"
939
+ ] })
940
+ ] }),
941
+ matched.controller_action && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
942
+ /* @__PURE__ */ u3("span", { children: "Controller#Action" }),
943
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--xs profiler-text--mono", children: matched.controller_action })
944
+ ] })
945
+ ] }) : /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: "No route matched" }) }) })
946
+ ] });
947
+ }
948
+
897
949
  // app/assets/typescript/profiler/components/toolbar/ToolbarApp.tsx
898
950
  function statusClass2(status) {
899
951
  if (status >= 200 && status < 300) return "profiler-text--success";
@@ -926,6 +978,7 @@
926
978
  const httpData = cd["http"];
927
979
  const logData = cd["logs"];
928
980
  const exceptionData = cd["exception"];
981
+ const routesData = cd["routes"];
929
982
  const reqClass = statusClass2(profile.status);
930
983
  const durClass = durationClass(profile.duration);
931
984
  const dbClass = (dbData?.slow_queries ?? 0) > 0 ? "profiler-text--error" : "profiler-text--success";
@@ -1121,6 +1174,18 @@
1121
1174
  ]
1122
1175
  }
1123
1176
  ),
1177
+ routesData && routesData.total > 0 && /* @__PURE__ */ u3(
1178
+ ToolbarItem,
1179
+ {
1180
+ href: `/_profiler/profiles/${token}?tab=routes`,
1181
+ panelLarge: true,
1182
+ panel: /* @__PURE__ */ u3(RoutesPanel, { routesData }),
1183
+ children: [
1184
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "ROUTE" }),
1185
+ /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: routesData.matched?.pattern ?? "\u2014" })
1186
+ ]
1187
+ }
1188
+ ),
1124
1189
  /* @__PURE__ */ u3("a", { href: "/_profiler", class: "profiler-toolbar-item", children: "\u2B21 Profiler" })
1125
1190
  ] });
1126
1191
  }
@@ -1107,6 +1107,49 @@ tr:hover .btn-row-delete {
1107
1107
  color: #fff;
1108
1108
  }
1109
1109
 
1110
+ .profiler-flex-table {
1111
+ display: flex;
1112
+ flex-direction: column;
1113
+ }
1114
+ .profiler-flex-table__header {
1115
+ display: flex;
1116
+ align-items: center;
1117
+ gap: var(--profiler-space-3);
1118
+ padding: var(--profiler-space-2) var(--profiler-space-3);
1119
+ background: var(--profiler-bg-light);
1120
+ border-bottom: 1px solid var(--profiler-border-strong);
1121
+ font-weight: 600;
1122
+ font-size: var(--profiler-text-xs);
1123
+ text-transform: uppercase;
1124
+ letter-spacing: 0.08em;
1125
+ color: var(--profiler-text-muted);
1126
+ }
1127
+ .profiler-flex-table__row {
1128
+ display: flex;
1129
+ align-items: center;
1130
+ gap: var(--profiler-space-3);
1131
+ padding: var(--profiler-space-3);
1132
+ border-bottom: 1px solid var(--profiler-border);
1133
+ color: var(--profiler-text);
1134
+ }
1135
+ .profiler-flex-table__row--matched {
1136
+ background: var(--profiler-success-bg);
1137
+ border-left: 2px solid var(--profiler-success);
1138
+ }
1139
+ .profiler-flex-table__cell {
1140
+ flex: 1;
1141
+ min-width: 0;
1142
+ overflow: hidden;
1143
+ text-overflow: ellipsis;
1144
+ white-space: nowrap;
1145
+ }
1146
+ .profiler-flex-table__cell--fixed {
1147
+ flex: 0 0 auto;
1148
+ }
1149
+ .profiler-flex-table__cell--muted {
1150
+ color: var(--profiler-text-muted);
1151
+ }
1152
+
1110
1153
  #profiler-toolbar {
1111
1154
  position: fixed;
1112
1155
  bottom: 0;
@@ -1174,6 +1174,7 @@
1174
1174
  function RequestTab({ profile }) {
1175
1175
  const [copied, setCopied] = d2(false);
1176
1176
  const curl = buildCurl(profile);
1177
+ const routeData = profile.collectors_data?.request ?? {};
1177
1178
  function copyToClipboard() {
1178
1179
  navigator.clipboard.writeText(curl).then(() => {
1179
1180
  setCopied(true);
@@ -1201,6 +1202,22 @@
1201
1202
  profile.duration.toFixed(2),
1202
1203
  " ms"
1203
1204
  ] })
1205
+ ] }),
1206
+ routeData.controller_action && /* @__PURE__ */ u3("tr", { children: [
1207
+ /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Controller#Action" }),
1208
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: routeData.controller_action })
1209
+ ] }),
1210
+ routeData.route_name && /* @__PURE__ */ u3("tr", { children: [
1211
+ /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Route Name" }),
1212
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: routeData.route_name })
1213
+ ] }),
1214
+ routeData.route_pattern && /* @__PURE__ */ u3("tr", { children: [
1215
+ /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Route Pattern" }),
1216
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: routeData.route_pattern })
1217
+ ] }),
1218
+ routeData.route_params && Object.keys(routeData.route_params).length > 0 && /* @__PURE__ */ u3("tr", { children: [
1219
+ /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Route Params" }),
1220
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: Object.entries(routeData.route_params).map(([k3, v3]) => `${k3}: ${v3}`).join(", ") })
1204
1221
  ] })
1205
1222
  ] }),
1206
1223
  profile.headers && Object.keys(profile.headers).length > 0 && /* @__PURE__ */ u3(k, { children: [
@@ -2289,6 +2306,119 @@
2289
2306
  ] });
2290
2307
  }
2291
2308
 
2309
+ // app/assets/typescript/profiler/components/dashboard/tabs/RoutesTab.tsx
2310
+ var VERB_COLORS = {
2311
+ GET: "var(--profiler-success)",
2312
+ POST: "var(--profiler-accent)",
2313
+ PUT: "var(--profiler-warning)",
2314
+ PATCH: "var(--profiler-warning)",
2315
+ DELETE: "var(--profiler-error)"
2316
+ };
2317
+ function VerbBadge({ verb }) {
2318
+ const color = VERB_COLORS[verb] ?? "var(--profiler-text-muted)";
2319
+ return /* @__PURE__ */ u3("span", { style: {
2320
+ display: "inline-block",
2321
+ minWidth: "52px",
2322
+ textAlign: "center",
2323
+ padding: "1px 6px",
2324
+ borderRadius: "4px",
2325
+ fontSize: "10px",
2326
+ fontWeight: 700,
2327
+ fontFamily: "monospace",
2328
+ border: `1px solid ${color}`,
2329
+ color
2330
+ }, children: verb });
2331
+ }
2332
+ function RoutesTab({ routesData }) {
2333
+ const [filter, setFilter] = d2("");
2334
+ const [verbFilter, setVerbFilter] = d2("ALL");
2335
+ const { total, matched, routes } = routesData;
2336
+ const verbs = ["ALL", ...Array.from(new Set(routes.map((r3) => r3.verb))).sort()];
2337
+ const filtered = routes.filter((route) => {
2338
+ const matchesVerb = verbFilter === "ALL" || route.verb === verbFilter;
2339
+ const q2 = filter.toLowerCase();
2340
+ const matchesText = !q2 || (route.pattern ?? "").toLowerCase().includes(q2) || (route.name ?? "").toLowerCase().includes(q2) || (route.controller_action ?? "").toLowerCase().includes(q2);
2341
+ return matchesVerb && matchesText;
2342
+ });
2343
+ return /* @__PURE__ */ u3(k, { children: [
2344
+ /* @__PURE__ */ u3("h2", { class: "profiler-section__header", children: "Routes" }),
2345
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mb-4", children: /* @__PURE__ */ u3("div", { class: "profiler-stat-card", children: [
2346
+ /* @__PURE__ */ u3("div", { class: "profiler-stat-card__value", children: total }),
2347
+ /* @__PURE__ */ u3("div", { class: "profiler-stat-card__label", children: "Total routes" })
2348
+ ] }) }),
2349
+ matched && /* @__PURE__ */ u3(k, { children: [
2350
+ /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-4", children: "Matched Route" }),
2351
+ /* @__PURE__ */ u3("table", { class: "profiler-mb-4", children: [
2352
+ /* @__PURE__ */ u3("tr", { children: [
2353
+ /* @__PURE__ */ u3("th", { class: "profiler-text--sm", style: "width:120px;", children: "Verb" }),
2354
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3(VerbBadge, { verb: matched.verb }) })
2355
+ ] }),
2356
+ /* @__PURE__ */ u3("tr", { children: [
2357
+ /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Pattern" }),
2358
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: matched.pattern })
2359
+ ] }),
2360
+ matched.name && /* @__PURE__ */ u3("tr", { children: [
2361
+ /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Route Name" }),
2362
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: [
2363
+ matched.name,
2364
+ "_path"
2365
+ ] })
2366
+ ] }),
2367
+ matched.controller_action && /* @__PURE__ */ u3("tr", { children: [
2368
+ /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Controller#Action" }),
2369
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: matched.controller_action })
2370
+ ] })
2371
+ ] })
2372
+ ] }),
2373
+ /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-4", children: "All Routes" }),
2374
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mb-3", style: "flex-wrap:wrap;align-items:center;", children: [
2375
+ /* @__PURE__ */ u3(
2376
+ "input",
2377
+ {
2378
+ type: "text",
2379
+ class: "profiler-filter-input",
2380
+ placeholder: "Filter by path, name or controller\u2026",
2381
+ value: filter,
2382
+ onInput: (e3) => setFilter(e3.target.value),
2383
+ style: "flex:1;width:auto;min-width:200px;"
2384
+ }
2385
+ ),
2386
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-1", children: verbs.map((v3) => /* @__PURE__ */ u3(
2387
+ "button",
2388
+ {
2389
+ onClick: () => setVerbFilter(v3),
2390
+ style: {
2391
+ padding: "3px 8px",
2392
+ fontSize: "11px",
2393
+ fontWeight: 600,
2394
+ borderRadius: "4px",
2395
+ border: "1px solid var(--profiler-border)",
2396
+ background: verbFilter === v3 ? "var(--profiler-accent)" : "transparent",
2397
+ color: verbFilter === v3 ? "#fff" : "var(--profiler-text-muted)",
2398
+ cursor: "pointer"
2399
+ },
2400
+ children: v3
2401
+ },
2402
+ v3
2403
+ )) })
2404
+ ] }),
2405
+ /* @__PURE__ */ u3("div", { class: "profiler-flex-table", children: [
2406
+ /* @__PURE__ */ u3("div", { class: "profiler-flex-table__header", children: [
2407
+ /* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell--fixed", children: "Verb" }),
2408
+ /* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell", children: "Pattern" }),
2409
+ /* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell", children: "Name" }),
2410
+ /* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell", children: "Controller#Action" })
2411
+ ] }),
2412
+ filtered.map((route, i3) => /* @__PURE__ */ u3("div", { class: `profiler-flex-table__row${route.matched ? " profiler-flex-table__row--matched" : ""}`, children: [
2413
+ /* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell--fixed", children: /* @__PURE__ */ u3(VerbBadge, { verb: route.verb }) }),
2414
+ /* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell profiler-text--mono profiler-text--xs", title: route.pattern, children: route.pattern }),
2415
+ /* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell profiler-flex-table__cell--muted profiler-text--mono profiler-text--xs", title: route.name ? `${route.name}_path` : "", children: route.name ? `${route.name}_path` : "\u2014" }),
2416
+ /* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell profiler-flex-table__cell--muted profiler-text--mono profiler-text--xs", title: route.controller_action ?? "", children: route.controller_action ?? "\u2014" })
2417
+ ] }, i3))
2418
+ ] })
2419
+ ] });
2420
+ }
2421
+
2292
2422
  // app/assets/typescript/profiler/components/dashboard/ProfileDashboard.tsx
2293
2423
  function ProfileDashboard({ profile, initialTab, embedded }) {
2294
2424
  const cd = profile.collectors_data || {};
@@ -2296,6 +2426,7 @@
2296
2426
  const hasHttp = cd["http"]?.total_requests > 0;
2297
2427
  const hasException = !!cd["exception"]?.exception_class;
2298
2428
  const hasLogs = (cd["logs"]?.count ?? 0) > 0;
2429
+ const hasRoutes = (cd["routes"]?.total ?? 0) > 0;
2299
2430
  const [activeTab, setActiveTab] = d2(hasException ? "exception" : initialTab);
2300
2431
  const handleTabClick = (tab) => (e3) => {
2301
2432
  e3.preventDefault();
@@ -2348,7 +2479,8 @@
2348
2479
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("timeline"), onClick: handleTabClick("timeline"), children: "Timeline" }),
2349
2480
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("views"), onClick: handleTabClick("views"), children: "Views" }),
2350
2481
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
2351
- hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" })
2482
+ hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
2483
+ hasRoutes && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("routes"), onClick: handleTabClick("routes"), children: "Routes" })
2352
2484
  ] }),
2353
2485
  /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
2354
2486
  activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
@@ -2360,7 +2492,8 @@
2360
2492
  activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, { flamegraphData: cd["flamegraph"], perfData: cd["performance"] }),
2361
2493
  activeTab === "views" && /* @__PURE__ */ u3(ViewsTab, { viewData: cd["view"] }),
2362
2494
  activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
2363
- activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] })
2495
+ activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
2496
+ activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] })
2364
2497
  ] })
2365
2498
  ] }),
2366
2499
  !embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
@@ -39,7 +39,7 @@ module Profiler
39
39
  end
40
40
 
41
41
  def has_data?
42
- @job_data.any?
42
+ @job_data.key?(:job_class)
43
43
  end
44
44
 
45
45
  def toolbar_summary
@@ -42,6 +42,7 @@ module Profiler
42
42
  finished_at: @profile.finished_at&.iso8601
43
43
  }
44
44
 
45
+ data.merge!(collect_route_info)
45
46
  store_data(data)
46
47
  end
47
48
 
@@ -64,6 +65,39 @@ module Profiler
64
65
 
65
66
  private
66
67
 
68
+ def collect_route_info
69
+ return {} unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
70
+
71
+ path = @profile.path
72
+ method = @profile.method
73
+
74
+ recognized = Rails.application.routes.recognize_path(path, method: method)
75
+
76
+ controller = recognized[:controller]
77
+ action = recognized[:action]
78
+
79
+ controller_action = if controller && action
80
+ "#{controller.split('/').map { |s| ActiveSupport::Inflector.camelize(s) }.join('::')}Controller##{action}"
81
+ end
82
+
83
+ route_params = recognized.except(:controller, :action, :format)
84
+
85
+ route_name, matched_route = Rails.application.routes.named_routes.find do |_name, route|
86
+ route.defaults[:controller] == controller &&
87
+ route.defaults[:action] == action &&
88
+ route.path.match(path)
89
+ end
90
+
91
+ {
92
+ route_name: route_name ? "#{route_name}_path" : nil,
93
+ route_pattern: matched_route&.path&.spec&.to_s&.sub(/\(\.:format\)$/, ""),
94
+ route_params: route_params,
95
+ controller_action: controller_action
96
+ }
97
+ rescue StandardError
98
+ {}
99
+ end
100
+
67
101
  def format_memory(bytes)
68
102
  return "0 B" unless bytes
69
103
 
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_collector"
4
+
5
+ module Profiler
6
+ module Collectors
7
+ class RoutesCollector < BaseCollector
8
+ def icon
9
+ "🗺️"
10
+ end
11
+
12
+ def priority
13
+ 12
14
+ end
15
+
16
+ def tab_config
17
+ {
18
+ key: "routes",
19
+ label: "Routes",
20
+ icon: icon,
21
+ priority: priority,
22
+ enabled: true,
23
+ default_active: false
24
+ }
25
+ end
26
+
27
+ def collect
28
+ unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
29
+ return store_data({ routes: [], total: 0 })
30
+ end
31
+
32
+ matched_controller, matched_action = recognize_current_route
33
+ routes = build_routes_list(matched_controller, matched_action)
34
+ matched_route = routes.find { |r| r[:matched] }
35
+
36
+ store_data({
37
+ total: routes.size,
38
+ matched: matched_route,
39
+ routes: routes
40
+ })
41
+ end
42
+
43
+ def toolbar_summary
44
+ data = panel_content
45
+ matched = data[:matched]
46
+ { text: matched ? matched[:pattern] : "—", color: "blue" }
47
+ end
48
+
49
+ private
50
+
51
+ def recognize_current_route
52
+ recognized = Rails.application.routes.recognize_path(@profile.path, method: @profile.method)
53
+ [recognized[:controller], recognized[:action]]
54
+ rescue StandardError
55
+ [nil, nil]
56
+ end
57
+
58
+ def build_routes_list(matched_controller, matched_action)
59
+ Rails.application.routes.routes.filter_map do |route|
60
+ next if route.respond_to?(:internal) && route.internal
61
+
62
+ controller = route.defaults[:controller]
63
+ action = route.defaults[:action]
64
+ next if controller.nil?
65
+ next if controller.start_with?("rails/", "profiler/")
66
+
67
+ name = route.name
68
+ pattern = route.path.spec.to_s.sub(/\(\.:format\)$/, "")
69
+ v = route.verb
70
+ verb = (v && !v.empty?) ? v : "ANY"
71
+
72
+ controller_action = if controller && action
73
+ "#{controller.split("/").map { |s| ActiveSupport::Inflector.camelize(s) }.join("::")}Controller##{action}"
74
+ end
75
+
76
+ {
77
+ name: name,
78
+ pattern: pattern,
79
+ verb: verb,
80
+ controller_action: controller_action,
81
+ matched: controller == matched_controller && action == matched_action
82
+ }
83
+ end
84
+ rescue StandardError
85
+ []
86
+ end
87
+ end
88
+ end
89
+ end
@@ -16,7 +16,9 @@ module Profiler
16
16
  duration: profile.duration&.round(2),
17
17
  memory: profile.memory ? (profile.memory / 1024.0 / 1024.0).round(2) : nil,
18
18
  timestamp: profile.started_at&.iso8601,
19
- query_count: profile.collector_data("database")&.dig("total_queries") || 0
19
+ query_count: profile.collector_data("database")&.dig("total_queries") || 0,
20
+ matched_route: profile.collector_data("routes")&.dig("matched", "pattern"),
21
+ controller_action: profile.collector_data("request")&.dig("controller_action")
20
22
  }
21
23
  end
22
24
 
@@ -266,6 +266,23 @@ module Profiler
266
266
  end
267
267
  end
268
268
 
269
+ # Routes section
270
+ routes_data = profile.collector_data("routes")
271
+ if routes_data && routes_data["total"].to_i > 0
272
+ lines << "## Routes"
273
+ lines << "- Total routes: #{routes_data['total']}"
274
+
275
+ matched = routes_data["matched"]
276
+ if matched
277
+ lines << "- **Matched:** `#{matched['verb']} #{matched['pattern']}`"
278
+ lines << " - Route name: #{matched['name']}_path" if matched["name"]
279
+ lines << " - Controller#Action: #{matched['controller_action']}" if matched["controller_action"]
280
+ else
281
+ lines << "- No route matched"
282
+ end
283
+ lines << ""
284
+ end
285
+
269
286
  # Dumps section
270
287
  dump_data = profile.collector_data("dump")
271
288
  if dump_data && dump_data["count"].to_i > 0
@@ -44,7 +44,8 @@ module Profiler
44
44
  Profiler::Collectors::CacheCollector,
45
45
  Profiler::Collectors::HttpCollector,
46
46
  Profiler::Collectors::FlameGraphCollector,
47
- Profiler::Collectors::LogCollector
47
+ Profiler::Collectors::LogCollector,
48
+ Profiler::Collectors::RoutesCollector
48
49
  ]
49
50
  end
50
51
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/profiler.rb CHANGED
@@ -65,6 +65,7 @@ require_relative "profiler/collectors/http_collector"
65
65
  require_relative "profiler/collectors/flamegraph_collector"
66
66
  require_relative "profiler/collectors/log_collector"
67
67
  require_relative "profiler/collectors/exception_collector"
68
+ require_relative "profiler/collectors/routes_collector"
68
69
 
69
70
  require_relative "profiler/railtie" if defined?(Rails::Railtie)
70
71
  require_relative "profiler/engine" if defined?(Rails::Engine)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-29 00:00:00.000000000 Z
11
+ date: 2026-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -133,6 +133,7 @@ files:
133
133
  - lib/profiler/collectors/log_collector.rb
134
134
  - lib/profiler/collectors/performance_collector.rb
135
135
  - lib/profiler/collectors/request_collector.rb
136
+ - lib/profiler/collectors/routes_collector.rb
136
137
  - lib/profiler/collectors/view_collector.rb
137
138
  - lib/profiler/configuration.rb
138
139
  - lib/profiler/engine.rb