rails-profiler 0.3.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: 84a94ffe07e5c055045a375cc92f69638618f23a169717b532cadcecc98ac95f
4
- data.tar.gz: c756cfb2bd7eff713976aaadc7e0d19375fc6747b47c6c421753dec3cc4bccdd
3
+ metadata.gz: 9e97a77a23dca55d709a1512c3c2ab3b95f0e6b15ed01b7769852587b962be8a
4
+ data.tar.gz: 131c40f561129e8835c41a0b3ba456431bb11fe791c8b0add6396fbbfe0be293
5
5
  SHA512:
6
- metadata.gz: e971cfd7febeafd91af85f6dbf922b0d092f76bb3c77223b9d00d1b682af6c33a4e1fa50905d426dd9326b8e1d13e22e09da3c9c32e05a194c9d9e539969a206
7
- data.tar.gz: f1a8b324dcae79a81bc63fa8a319f815c0610dbaa606c5db1c41c4d5226a69812a4946cb85987aa7e7a452a098478d3f61c61b17178b2dc396d745c7e77b26f1
6
+ metadata.gz: 7565ae06d41a7bd2d574610962e20fc3c4a6fcba5bda89482e840fb09af57c686eff9e8f4ccb02bfde05d53e0b5cc8019622ee7ea4db44fc85d6f5a0bc09c8c6
7
+ data.tar.gz: bea8aee55cc58b41fc6d01c2ad4dffcdd9ecfdfc4617a45ba16b019577ad5209a2bcae5c9d226d0d5e31af1fbb3e508db8a7e3eae25c5c87f920dcad3e48d1f8
@@ -946,6 +946,46 @@
946
946
  ] });
947
947
  }
948
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
+
949
989
  // app/assets/typescript/profiler/components/toolbar/ToolbarApp.tsx
950
990
  function statusClass2(status) {
951
991
  if (status >= 200 && status < 300) return "profiler-text--success";
@@ -979,6 +1019,7 @@
979
1019
  const logData = cd["logs"];
980
1020
  const exceptionData = cd["exception"];
981
1021
  const routesData = cd["routes"];
1022
+ const i18nData = cd["i18n"];
982
1023
  const reqClass = statusClass2(profile.status);
983
1024
  const durClass = durationClass(profile.duration);
984
1025
  const dbClass = (dbData?.slow_queries ?? 0) > 0 ? "profiler-text--error" : "profiler-text--success";
@@ -1186,6 +1227,22 @@
1186
1227
  ]
1187
1228
  }
1188
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
+ ),
1189
1246
  /* @__PURE__ */ u3("a", { href: "/_profiler", class: "profiler-toolbar-item", children: "\u2B21 Profiler" })
1190
1247
  ] });
1191
1248
  }
@@ -2419,6 +2419,63 @@
2419
2419
  ] });
2420
2420
  }
2421
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
+
2422
2479
  // app/assets/typescript/profiler/components/dashboard/ProfileDashboard.tsx
2423
2480
  function ProfileDashboard({ profile, initialTab, embedded }) {
2424
2481
  const cd = profile.collectors_data || {};
@@ -2427,6 +2484,7 @@
2427
2484
  const hasException = !!cd["exception"]?.exception_class;
2428
2485
  const hasLogs = (cd["logs"]?.count ?? 0) > 0;
2429
2486
  const hasRoutes = (cd["routes"]?.total ?? 0) > 0;
2487
+ const hasI18n = (cd["i18n"]?.total ?? 0) > 0;
2430
2488
  const [activeTab, setActiveTab] = d2(hasException ? "exception" : initialTab);
2431
2489
  const handleTabClick = (tab) => (e3) => {
2432
2490
  e3.preventDefault();
@@ -2480,7 +2538,8 @@
2480
2538
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("views"), onClick: handleTabClick("views"), children: "Views" }),
2481
2539
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
2482
2540
  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" })
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" })
2484
2543
  ] }),
2485
2544
  /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
2486
2545
  activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
@@ -2493,7 +2552,8 @@
2493
2552
  activeTab === "views" && /* @__PURE__ */ u3(ViewsTab, { viewData: cd["view"] }),
2494
2553
  activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
2495
2554
  activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
2496
- activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] })
2555
+ activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
2556
+ activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] })
2497
2557
  ] })
2498
2558
  ] }),
2499
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
@@ -45,7 +45,8 @@ module Profiler
45
45
  Profiler::Collectors::HttpCollector,
46
46
  Profiler::Collectors::FlameGraphCollector,
47
47
  Profiler::Collectors::LogCollector,
48
- Profiler::Collectors::RoutesCollector
48
+ Profiler::Collectors::RoutesCollector,
49
+ Profiler::Collectors::I18nCollector
49
50
  ]
50
51
  end
51
52
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/profiler.rb CHANGED
@@ -66,6 +66,7 @@ require_relative "profiler/collectors/flamegraph_collector"
66
66
  require_relative "profiler/collectors/log_collector"
67
67
  require_relative "profiler/collectors/exception_collector"
68
68
  require_relative "profiler/collectors/routes_collector"
69
+ require_relative "profiler/collectors/i18n_collector"
69
70
 
70
71
  require_relative "profiler/railtie" if defined?(Rails::Railtie)
71
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.3.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-04-01 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,6 +129,7 @@ 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