rails-profiler 0.2.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59da55aba7ca1284fdda2c948c456bffc05de245d876dba9c9b477257f56654d
4
- data.tar.gz: e3df3028c4a547e83cda3cb2d6170c279c059d140725efb38a489022e02d15e9
3
+ metadata.gz: 9e97a77a23dca55d709a1512c3c2ab3b95f0e6b15ed01b7769852587b962be8a
4
+ data.tar.gz: 131c40f561129e8835c41a0b3ba456431bb11fe791c8b0add6396fbbfe0be293
5
5
  SHA512:
6
- metadata.gz: f04910c4c4592f4ee1168c5010330d36d460deb8b7ef890ecc4680013c107aaba9c627cbdeb5674a401379b457e0ea13077dcafb5490ca2124804be152fadeb7
7
- data.tar.gz: d50f9dcded83688a09009c5aa5436e7726007b7b72e44b78d4107d11ddbd72046eef1e812bf606804cb1965fd58ccdae999b7542651baf0a5ddd280c7b2812a4
6
+ metadata.gz: 7565ae06d41a7bd2d574610962e20fc3c4a6fcba5bda89482e840fb09af57c686eff9e8f4ccb02bfde05d53e0b5cc8019622ee7ea4db44fc85d6f5a0bc09c8c6
7
+ data.tar.gz: bea8aee55cc58b41fc6d01c2ad4dffcdd9ecfdfc4617a45ba16b019577ad5209a2bcae5c9d226d0d5e31af1fbb3e508db8a7e3eae25c5c87f920dcad3e48d1f8
@@ -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,78 @@
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
+
949
+ // app/assets/typescript/profiler/components/toolbar/panels/I18nPanel.tsx
950
+ function I18nPanel({ i18nData }) {
951
+ const missing = i18nData.lookups.filter((l3) => l3.missing);
952
+ const ok = i18nData.lookups.filter((l3) => !l3.missing);
953
+ const prioritized = [...missing, ...ok].slice(0, 5);
954
+ const remaining = i18nData.total - 5;
955
+ return /* @__PURE__ */ u3(k, { children: [
956
+ /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-header", children: [
957
+ "I18n",
958
+ /* @__PURE__ */ u3("span", { style: "margin-left:6px;font-weight:400;color:var(--profiler-muted,#6b7280);", children: [
959
+ "[",
960
+ i18nData.locale,
961
+ "]"
962
+ ] }),
963
+ i18nData.missing_count > 0 && /* @__PURE__ */ u3("span", { style: "color:var(--profiler-error,#ef4444);margin-left:8px;", children: [
964
+ i18nData.missing_count,
965
+ " missing"
966
+ ] })
967
+ ] }),
968
+ /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-content", children: [
969
+ prioritized.map((entry, i3) => /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", style: "align-items:flex-start;gap:6px;", children: [
970
+ /* @__PURE__ */ u3(
971
+ "span",
972
+ {
973
+ class: "profiler-text--xs profiler-text--truncate",
974
+ style: `flex:1;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;${entry.missing ? "color:var(--profiler-error,#ef4444);" : ""}`,
975
+ children: entry.key
976
+ }
977
+ ),
978
+ entry.missing && /* @__PURE__ */ u3("span", { class: "profiler-text--xs", style: "color:var(--profiler-error,#ef4444);font-weight:600;", children: "\u26A0" })
979
+ ] }, i3)),
980
+ remaining > 0 && /* @__PURE__ */ u3("div", { class: "profiler-more", children: [
981
+ "+ ",
982
+ remaining,
983
+ " more keys"
984
+ ] })
985
+ ] })
986
+ ] });
987
+ }
988
+
897
989
  // app/assets/typescript/profiler/components/toolbar/ToolbarApp.tsx
898
990
  function statusClass2(status) {
899
991
  if (status >= 200 && status < 300) return "profiler-text--success";
@@ -926,6 +1018,8 @@
926
1018
  const httpData = cd["http"];
927
1019
  const logData = cd["logs"];
928
1020
  const exceptionData = cd["exception"];
1021
+ const routesData = cd["routes"];
1022
+ const i18nData = cd["i18n"];
929
1023
  const reqClass = statusClass2(profile.status);
930
1024
  const durClass = durationClass(profile.duration);
931
1025
  const dbClass = (dbData?.slow_queries ?? 0) > 0 ? "profiler-text--error" : "profiler-text--success";
@@ -1121,6 +1215,34 @@
1121
1215
  ]
1122
1216
  }
1123
1217
  ),
1218
+ routesData && routesData.total > 0 && /* @__PURE__ */ u3(
1219
+ ToolbarItem,
1220
+ {
1221
+ href: `/_profiler/profiles/${token}?tab=routes`,
1222
+ panelLarge: true,
1223
+ panel: /* @__PURE__ */ u3(RoutesPanel, { routesData }),
1224
+ children: [
1225
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "ROUTE" }),
1226
+ /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: routesData.matched?.pattern ?? "\u2014" })
1227
+ ]
1228
+ }
1229
+ ),
1230
+ i18nData && i18nData.total > 0 && /* @__PURE__ */ u3(
1231
+ ToolbarItem,
1232
+ {
1233
+ href: `/_profiler/profiles/${token}?tab=i18n`,
1234
+ className: i18nData.missing_count > 0 ? "profiler-text--error" : "profiler-text--muted",
1235
+ panel: /* @__PURE__ */ u3(I18nPanel, { i18nData }),
1236
+ children: [
1237
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "I18N" }),
1238
+ /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: i18nData.locale }),
1239
+ i18nData.missing_count > 0 && /* @__PURE__ */ u3("span", { class: "profiler-text--error profiler-text--xs", children: [
1240
+ "\u26A0 ",
1241
+ i18nData.missing_count
1242
+ ] })
1243
+ ]
1244
+ }
1245
+ ),
1124
1246
  /* @__PURE__ */ u3("a", { href: "/_profiler", class: "profiler-toolbar-item", children: "\u2B21 Profiler" })
1125
1247
  ] });
1126
1248
  }
@@ -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,176 @@
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
+
2422
+ // app/assets/typescript/profiler/components/dashboard/tabs/I18nTab.tsx
2423
+ function I18nTab({ i18nData }) {
2424
+ const [filter, setFilter] = d2("ALL");
2425
+ if (!i18nData?.lookups?.length) {
2426
+ return /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
2427
+ /* @__PURE__ */ u3("div", { class: "profiler-empty__icon", children: "\u{1F310}" }),
2428
+ /* @__PURE__ */ u3("h3", { class: "profiler-empty__title", children: "No translation lookups captured" }),
2429
+ /* @__PURE__ */ u3("div", { class: "profiler-empty__description", children: /* @__PURE__ */ u3("p", { children: [
2430
+ "Calls to ",
2431
+ /* @__PURE__ */ u3("code", { children: "I18n.t" }),
2432
+ " during this request will appear here."
2433
+ ] }) })
2434
+ ] });
2435
+ }
2436
+ const filtered = filter === "MISSING" ? i18nData.lookups.filter((l3) => l3.missing) : i18nData.lookups;
2437
+ return /* @__PURE__ */ u3(k, { children: [
2438
+ /* @__PURE__ */ u3("h2", { class: "profiler-section__header", children: [
2439
+ "I18n Lookups (",
2440
+ i18nData.total,
2441
+ ")"
2442
+ ] }),
2443
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mb-4 profiler-text--sm", children: [
2444
+ /* @__PURE__ */ u3("span", { children: [
2445
+ "Locale: ",
2446
+ /* @__PURE__ */ u3("strong", { children: i18nData.locale })
2447
+ ] }),
2448
+ i18nData.missing_count > 0 && /* @__PURE__ */ u3("span", { children: [
2449
+ "Missing: ",
2450
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--error", children: i18nData.missing_count })
2451
+ ] })
2452
+ ] }),
2453
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mb-4", children: ["ALL", "MISSING"].map((f4) => /* @__PURE__ */ u3(
2454
+ "button",
2455
+ {
2456
+ onClick: () => setFilter(f4),
2457
+ class: `btn btn-sm ${filter === f4 ? "btn-primary" : "btn-secondary"}`,
2458
+ children: f4
2459
+ },
2460
+ f4
2461
+ )) }),
2462
+ filtered.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-text--muted profiler-text--sm", children: "No missing translations." }) : /* @__PURE__ */ u3("table", { class: "profiler-table", children: [
2463
+ /* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: [
2464
+ /* @__PURE__ */ u3("th", { children: "Key" }),
2465
+ /* @__PURE__ */ u3("th", { children: "Locale" }),
2466
+ /* @__PURE__ */ u3("th", { children: "Value" }),
2467
+ /* @__PURE__ */ u3("th", { children: "Status" })
2468
+ ] }) }),
2469
+ /* @__PURE__ */ u3("tbody", { children: filtered.map((entry, index) => /* @__PURE__ */ u3("tr", { style: entry.missing ? "background:var(--profiler-error-bg,rgba(239,68,68,0.08));" : "", children: [
2470
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("code", { class: `profiler-text--xs profiler-text--mono${entry.missing ? " profiler-text--error" : ""}`, children: entry.key }) }),
2471
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", children: entry.locale }) }),
2472
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: `profiler-text--xs${entry.missing ? " profiler-text--error" : ""}`, children: entry.value }) }),
2473
+ /* @__PURE__ */ u3("td", { children: entry.missing ? /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--error", style: "font-weight:600;", children: "\u26A0 missing" }) : /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--success", children: "\u2713" }) })
2474
+ ] }, index)) })
2475
+ ] })
2476
+ ] });
2477
+ }
2478
+
2292
2479
  // app/assets/typescript/profiler/components/dashboard/ProfileDashboard.tsx
2293
2480
  function ProfileDashboard({ profile, initialTab, embedded }) {
2294
2481
  const cd = profile.collectors_data || {};
@@ -2296,6 +2483,8 @@
2296
2483
  const hasHttp = cd["http"]?.total_requests > 0;
2297
2484
  const hasException = !!cd["exception"]?.exception_class;
2298
2485
  const hasLogs = (cd["logs"]?.count ?? 0) > 0;
2486
+ const hasRoutes = (cd["routes"]?.total ?? 0) > 0;
2487
+ const hasI18n = (cd["i18n"]?.total ?? 0) > 0;
2299
2488
  const [activeTab, setActiveTab] = d2(hasException ? "exception" : initialTab);
2300
2489
  const handleTabClick = (tab) => (e3) => {
2301
2490
  e3.preventDefault();
@@ -2348,7 +2537,9 @@
2348
2537
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("timeline"), onClick: handleTabClick("timeline"), children: "Timeline" }),
2349
2538
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("views"), onClick: handleTabClick("views"), children: "Views" }),
2350
2539
  /* @__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" })
2540
+ hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
2541
+ hasRoutes && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("routes"), onClick: handleTabClick("routes"), children: "Routes" }),
2542
+ hasI18n && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("i18n"), onClick: handleTabClick("i18n"), children: "I18n" })
2352
2543
  ] }),
2353
2544
  /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
2354
2545
  activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
@@ -2360,7 +2551,9 @@
2360
2551
  activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, { flamegraphData: cd["flamegraph"], perfData: cd["performance"] }),
2361
2552
  activeTab === "views" && /* @__PURE__ */ u3(ViewsTab, { viewData: cd["view"] }),
2362
2553
  activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
2363
- activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] })
2554
+ activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
2555
+ activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
2556
+ activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] })
2364
2557
  ] })
2365
2558
  ] }),
2366
2559
  !embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_collector"
4
+
5
+ module Profiler
6
+ module I18nLookupTracker
7
+ def translate(key, **options)
8
+ result = super
9
+ if (collector = Thread.current[:profiler_i18n_collector])
10
+ missing = result.is_a?(String) && result.downcase.include?("translation missing:")
11
+ collector.record_lookup(key, options[:locale] || I18n.locale, result, missing)
12
+ end
13
+ result
14
+ rescue I18n::MissingTranslationData => e
15
+ if (collector = Thread.current[:profiler_i18n_collector])
16
+ collector.record_lookup(key, options[:locale] || I18n.locale, nil, true)
17
+ end
18
+ raise
19
+ end
20
+ alias_method :t, :translate
21
+ end
22
+
23
+ module Collectors
24
+ class I18nCollector < BaseCollector
25
+ def initialize(profile)
26
+ super
27
+ @lookups = []
28
+ end
29
+
30
+ def icon
31
+ "🌐"
32
+ end
33
+
34
+ def priority
35
+ 45
36
+ end
37
+
38
+ def tab_config
39
+ {
40
+ key: "i18n",
41
+ label: "I18n",
42
+ icon: icon,
43
+ priority: priority,
44
+ enabled: true,
45
+ default_active: false
46
+ }
47
+ end
48
+
49
+ def subscribe
50
+ return unless defined?(I18n)
51
+
52
+ unless I18n.singleton_class.ancestors.include?(Profiler::I18nLookupTracker)
53
+ I18n.singleton_class.prepend(Profiler::I18nLookupTracker)
54
+ end
55
+
56
+ Thread.current[:profiler_i18n_collector] = self
57
+ end
58
+
59
+ def collect
60
+ Thread.current[:profiler_i18n_collector] = nil
61
+
62
+ missing_count = @lookups.count { |l| l[:missing] }
63
+
64
+ store_data(
65
+ locale: I18n.locale.to_s,
66
+ total: @lookups.size,
67
+ missing_count: missing_count,
68
+ lookups: @lookups
69
+ )
70
+ end
71
+
72
+ def record_lookup(key, locale, result, missing = false)
73
+ value = missing ? "[missing]" : truncate(result.to_s)
74
+
75
+ @lookups << {
76
+ key: key.to_s,
77
+ locale: locale.to_s,
78
+ value: value,
79
+ missing: missing
80
+ }
81
+ end
82
+
83
+ def toolbar_summary
84
+ missing = @lookups.count { |l| l[:missing] }
85
+ locale = I18n.locale.to_s
86
+ total = @lookups.size
87
+
88
+ color = missing > 0 ? "red" : "green"
89
+
90
+ { text: "#{locale} · #{total} keys", color: color }
91
+ end
92
+
93
+ private
94
+
95
+ def truncate(str, max = 100)
96
+ str.length > max ? "#{str[0, max]}…" : str
97
+ end
98
+ end
99
+ end
100
+ end
@@ -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,9 @@ 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,
49
+ Profiler::Collectors::I18nCollector
48
50
  ]
49
51
  end
50
52
  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.4.0"
5
5
  end
data/lib/profiler.rb CHANGED
@@ -65,6 +65,8 @@ 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"
69
+ require_relative "profiler/collectors/i18n_collector"
68
70
 
69
71
  require_relative "profiler/railtie" if defined?(Rails::Railtie)
70
72
  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.4.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-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -129,10 +129,12 @@ files:
129
129
  - lib/profiler/collectors/exception_collector.rb
130
130
  - lib/profiler/collectors/flamegraph_collector.rb
131
131
  - lib/profiler/collectors/http_collector.rb
132
+ - lib/profiler/collectors/i18n_collector.rb
132
133
  - lib/profiler/collectors/job_collector.rb
133
134
  - lib/profiler/collectors/log_collector.rb
134
135
  - lib/profiler/collectors/performance_collector.rb
135
136
  - lib/profiler/collectors/request_collector.rb
137
+ - lib/profiler/collectors/routes_collector.rb
136
138
  - lib/profiler/collectors/view_collector.rb
137
139
  - lib/profiler/configuration.rb
138
140
  - lib/profiler/engine.rb