rails-profiler 0.32.1 → 0.32.3

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: 696b15a9e7e1409bea43ef0541724ca559955c2dba15643c5743bf606c62ba60
4
- data.tar.gz: 4091f3101110253c4ff00c32a0010e8b8097c329b7b2af10e91f13df82a15dbe
3
+ metadata.gz: 97fa343641a35a6bb20283e405ed8955b7039d4da924a7ce4195feeaeffacec6
4
+ data.tar.gz: d7f9d92015e47e0f20c2b344e2d297b169fcf5d9b184aad17f8d3c68f109f5f2
5
5
  SHA512:
6
- metadata.gz: 101f907c26e725cafa2d4fb70cab3ae957e0e3db9ddca412220137ae66af242f8c8397646bb8c239999e6d5b4d6121bf1c1b73169fe3c8ce982f7fbf2dcd066c
7
- data.tar.gz: f966f148cd6cbb34867691d0f18549190b15e5059e21b87adbdf457b5b62e8cceeeac195f0693fe206e9117440e940d0f96aac4a25c8c411cfc84bec28ec5fb5
6
+ metadata.gz: cd484fea1576a382bc4a6ecef0a39bad2d25b0197d672c62b41e83fed0ef39dfbaf96e8d6100d9d34ed8fca48d765dacbf97099b8963f505503cfc56058be646
7
+ data.tar.gz: de36e884fb0fd828d6d498c93858c73998762ec5823c7be1806897bd66ab36a97e61c2a1aa9d5a854694a6d184495855c812afcc0b6646cfe5318aac0f0dd037
data/CHANGELOG.md CHANGED
@@ -19,6 +19,49 @@ every commit of every tag interval is accounted for one way or the other.
19
19
 
20
20
  ## [Unreleased]
21
21
 
22
+ ## [0.32.3] - 2026-10-14
23
+
24
+ <!-- stamped -->
25
+
26
+ ### Fixed
27
+
28
+ - **Jobs:** the toolbar of a request now sees the jobs it enqueued start and end after its
29
+ response, without a page reload. A job enqueued by a request is saved as `running` as soon as
30
+ it starts, so it appears in the `child_jobs` of that request while it runs, and every save of
31
+ the job tells the request's toolbar that its profile changed, so it reads the job again when it
32
+ ends, `completed` or `failed`. Without Redis storage, this reaches the toolbar only when the job
33
+ runs in a process serving the profiler's pages (the `async` or `inline` adapters); a job run by
34
+ another process is still listed on the next reload.
35
+ With several Puma workers and no Redis, the toolbar sees a job start and end only when its
36
+ polling happens to reach the worker that ran the job.
37
+ - **Jobs:** a job that is still running is shown as running, in the JOB panel of the toolbar and
38
+ in the Background Jobs list of `/_profiler`: a turning ◐ with `running` and no duration, a blue
39
+ **Running** badge in the list, instead of a green check mark and a duration of 0 ms that read as
40
+ an instant success. The page of the job itself and the Jobs tab of a profile do the same: a blue
41
+ **Running** badge and no duration. The JOB item of the toolbar is blue while one of its jobs runs, the list has
42
+ a **Running** status filter, and its **Completed** filter no longer includes running jobs. The
43
+ `status` filter of the MCP tool `query_jobs` accepts `running`. A job killed while it performs
44
+ (`SIGKILL`, out of memory, container stopped) never records its end, so it stays `running` in the
45
+ toolbar and the list for good.
46
+
47
+ ## [0.32.2] - 2026-10-13
48
+
49
+ <!-- stamped -->
50
+
51
+ ### Fixed
52
+
53
+ - **Test profiling:** `Profiler::TestHelpers::RSpecSupport.install` no longer makes every RSpec
54
+ example fail with `NoMethodError: undefined method 'line_number'`. The workaround documented
55
+ for 0.32.1, a hand-written `around` hook, can be replaced by `install` again.
56
+ - **Test profiling:** Failing and pending RSpec examples are recorded with their real status,
57
+ `failed` or `pending`, instead of `passed`, in the test profiles and in the report printed
58
+ after the suite. A failure keeps its message, and an example marked `pending` or `skip` in its
59
+ body keeps its reason. This holds for an example tagged `:aggregate_failures` and for a failure
60
+ raised by an `around` hook of the suite, which the profiler now wraps.
61
+ - **Test profiling:** With Minitest, an error raised by the profiler no longer runs the test a
62
+ second time, unprofiled, while hiding the error. The test fails with that error, once, and the
63
+ run goes on with the next tests.
64
+
22
65
  ## [0.32.1] - 2026-10-13
23
66
 
24
67
  <!-- stamped -->
@@ -847,323 +890,323 @@ every commit of every tag interval is accounted for one way or the other.
847
890
 
848
891
  ### Fixed
849
892
 
850
- - **Cluster:** Resolve profiles across slaves from the master `ProfilesController` <!-- 9236c4c -->
893
+ - **Cluster:** Resolve profiles across slaves from the master `ProfilesController` <!-- 57d379f -->
851
894
 
852
895
  ## [0.30.3] - 2026-06-29
853
896
 
854
897
  ### Fixed
855
898
 
856
- - Wrap the profile dashboards and `TestRunnerPage` with `QueryClientProvider` <!-- 420ae2f -->
899
+ - Wrap the profile dashboards and `TestRunnerPage` with `QueryClientProvider` <!-- e5ad047 -->
857
900
 
858
901
  ## [0.30.2] - 2026-06-29
859
902
 
860
903
  ### Fixed
861
904
 
862
- - **Middleware:** Duplicate downstream headers before mutating them <!-- 3efb20c -->
905
+ - **Middleware:** Duplicate downstream headers before mutating them <!-- 01cea9c -->
863
906
 
864
907
  ## [0.30.1] - 2026-06-29
865
908
 
866
909
  ### Fixed
867
910
 
868
- - **Cluster:** Fix sentinel mismatch, profile list pagination, nil safety in MCP tools and HTTPS slave URLs <!-- 4b5fb1c -->
911
+ - **Cluster:** Fix sentinel mismatch, profile list pagination, nil safety in MCP tools and HTTPS slave URLs <!-- a8a508b -->
869
912
 
870
913
  ## [0.30.0] - 2026-06-28
871
914
 
872
915
  ### Added
873
916
 
874
- - **Cluster:** Add master/slave profiler clustering <!-- 1435f22 -->
917
+ - **Cluster:** Add master/slave profiler clustering <!-- 8109047 -->
875
918
 
876
919
  ## [0.29.0] - 2026-06-28
877
920
 
878
921
  ### Added
879
922
 
880
- - **SSE:** Push live profile updates over server-sent events <!-- fa0ae2e -->
923
+ - **SSE:** Push live profile updates over server-sent events <!-- ff431f6 -->
881
924
 
882
925
  ## [0.28.0] - 2026-06-06
883
926
 
884
927
  ### Added
885
928
 
886
- - **HTTP:** Add the `http_backtrace_depth` configuration option <!-- 25b4f99 -->
929
+ - **HTTP:** Add the `http_backtrace_depth` configuration option <!-- e15864c -->
887
930
 
888
931
  ## [0.27.1] - 2026-06-06
889
932
 
890
933
  ### Changed
891
934
 
892
- - **API:** Replace manual `fetch()` calls with `orval` generated TanStack Query hooks <!-- e33341e -->
935
+ - **API:** Replace manual `fetch()` calls with `orval` generated TanStack Query hooks <!-- cc3e2ca -->
893
936
 
894
937
  ### Fixed
895
938
 
896
- - **File store:** Prevent `ENOENT` errors on concurrent profile file deletion <!-- 8898f0e -->
939
+ - **File store:** Prevent `ENOENT` errors on concurrent profile file deletion <!-- d1dd476 -->
897
940
 
898
941
  ## [0.27.0] - 2026-06-02
899
942
 
900
943
  ### Added
901
944
 
902
- - **MCP:** Add console profiling, mailer detail, logs/env/i18n sections and full documentation <!-- ca362a1 -->
945
+ - **MCP:** Add console profiling, mailer detail, logs/env/i18n sections and full documentation <!-- 9f97a3f -->
903
946
 
904
947
  ## [0.26.0] - 2026-06-02
905
948
 
906
949
  ### Added
907
950
 
908
- - **Test profiler:** Add the `run_tests` MCP tool, the `track_tests` option, specs and docs <!-- 265ce03 -->
909
- - **Test profiler:** Enrich reporter, add SSE streaming, MCP tools and env safety <!-- a410c4b -->
910
- - **Test profiler:** Add test profiling, test runner UI and MCP tool <!-- a116fcb -->
951
+ - **Test profiler:** Add the `run_tests` MCP tool, the `track_tests` option, specs and docs <!-- 5c63fc0 -->
952
+ - **Test profiler:** Enrich reporter, add SSE streaming, MCP tools and env safety <!-- 58b2d8b -->
953
+ - **Test profiler:** Add test profiling, test runner UI and MCP tool <!-- 4c50149 -->
911
954
 
912
955
  ## [0.25.0] - 2026-06-01
913
956
 
914
957
  ### Added
915
958
 
916
- - **Console:** Add a console profiling tab with IRB instrumentation <!-- f29937a -->
959
+ - **Console:** Add a console profiling tab with IRB instrumentation <!-- c6ce1b7 -->
917
960
 
918
961
  ### Changed
919
962
 
920
- - **Docs:** Document the console profiling tab and the env override behaviour <!-- 031cac6 -->
963
+ - **Docs:** Document the console profiling tab and the env override behaviour <!-- e4f6fe9 -->
921
964
 
922
965
  <!-- excluded:
923
- bd55e9b specs only, no visible change
966
+ e8941a4 specs only, no visible change
924
967
  -->
925
968
 
926
969
  ## [0.24.0] - 2026-06-01
927
970
 
928
971
  ### Added
929
972
 
930
- - **MCP:** Add tools to manage `ENV` variables <!-- 9126194 -->
973
+ - **MCP:** Add tools to manage `ENV` variables <!-- 52f43ce -->
931
974
 
932
975
  ## [0.23.0] - 2026-05-27
933
976
 
934
977
  ### Added
935
978
 
936
- - **Profile list:** Add a global clear-all button and auto-refresh on tab switch <!-- 460a110 -->
979
+ - **Profile list:** Add a global clear-all button and auto-refresh on tab switch <!-- b85fc97 -->
937
980
 
938
981
  ## [0.22.1] - 2026-05-26
939
982
 
940
983
  ### Fixed
941
984
 
942
- - **HTTP:** Propagate collector to child threads, add fire-and-forget support, fix deep backtrace capture <!-- 9d0bd61 -->
985
+ - **HTTP:** Propagate collector to child threads, add fire-and-forget support, fix deep backtrace capture <!-- 8fd3f63 -->
943
986
 
944
987
  ## [0.22.0] - 2026-05-04
945
988
 
946
989
  ### Added
947
990
 
948
- - **Mailer:** Add a body preview, a variables display, a queued badge and design alignment <!-- b7d608f -->
949
- - **Collector:** Implement `MailerCollector`, tracking ActionMailer deliveries <!-- e59dd72 -->
991
+ - **Mailer:** Add a body preview, a variables display, a queued badge and design alignment <!-- d016698 -->
992
+ - **Collector:** Implement `MailerCollector`, tracking ActionMailer deliveries <!-- b1705c9 -->
950
993
 
951
994
  ### Changed
952
995
 
953
- - **Toolbar:** Remove the emoji from the mailer toolbar item and the dashboard tab <!-- 55df9bb -->
996
+ - **Toolbar:** Remove the emoji from the mailer toolbar item and the dashboard tab <!-- f18e615 -->
954
997
 
955
998
  ### Fixed
956
999
 
957
- - **Mailer:** Add the Mailers tab to `JobProfileDashboard` <!-- 4ce145d -->
958
- - **Mailer:** Fix thread isolation and `deliver_later` mode detection <!-- 6e76a99 -->
959
- - **Mailer:** Add `MailerCollector` to the job profiler and align the toolbar item design <!-- 915be28 -->
960
- - **Mailer:** Handle the Rails 7 encoded mail string in the `deliver.action_mailer` payload <!-- 5479dcf -->
961
- - **Mailer:** Replace `Thread.current.delete` with a read and nil write for fiber local variables <!-- 8787201 -->
962
- - **Version:** Remove the pre-release suffix, which broke Bundler resolution <!-- 98caecd -->
963
- - **Mailer:** Address code review findings from MR !32 <!-- b46d3d2 -->
1000
+ - **Mailer:** Add the Mailers tab to `JobProfileDashboard` <!-- 05f0549 -->
1001
+ - **Mailer:** Fix thread isolation and `deliver_later` mode detection <!-- 35c9de3 -->
1002
+ - **Mailer:** Add `MailerCollector` to the job profiler and align the toolbar item design <!-- e566a77 -->
1003
+ - **Mailer:** Handle the Rails 7 encoded mail string in the `deliver.action_mailer` payload <!-- c5519ce -->
1004
+ - **Mailer:** Replace `Thread.current.delete` with a read and nil write for fiber local variables <!-- 2828c44 -->
1005
+ - **Version:** Remove the pre-release suffix, which broke Bundler resolution <!-- eba7e9f -->
1006
+ - **Mailer:** Address code review findings from MR !32 <!-- 655d090 -->
964
1007
 
965
1008
  ## [0.21.0] - 2026-05-02
966
1009
 
967
1010
  ### Added
968
1011
 
969
- - **Toolbar:** Add a collapse/expand toggle <!-- 8f0669a -->
1012
+ - **Toolbar:** Add a collapse/expand toggle <!-- 7bc637a -->
970
1013
 
971
1014
  ## [0.20.0] - 2026-05-01
972
1015
 
973
1016
  ### Added
974
1017
 
975
- - **UI:** Display the gem version and warn on a mismatch <!-- 64ff6eb -->
1018
+ - **UI:** Display the gem version and warn on a mismatch <!-- a2509c2 -->
976
1019
 
977
1020
  ## [0.19.2] - 2026-04-24
978
1021
 
979
1022
  ### Fixed
980
1023
 
981
- - **HTTP:** Capture `RestClient` `body_stream`, remove body size limits, show bodies in MCP tool results <!-- 89fb0e5 -->
1024
+ - **HTTP:** Capture `RestClient` `body_stream`, remove body size limits, show bodies in MCP tool results <!-- 20654f5 -->
982
1025
 
983
1026
  <!-- excluded:
984
- 4527e12 release plumbing, version placeholder reset
1027
+ a9f2cd3 release plumbing, version placeholder reset
985
1028
  -->
986
1029
 
987
1030
  ## [0.19.1] - 2026-04-17
988
1031
 
989
1032
  ### Fixed
990
1033
 
991
- - **Env vars:** Fix revert restoring wrong value, SQLite busy errors, middleware rescue handling and UI refresh <!-- 55c8661 -->
1034
+ - **Env vars:** Fix revert restoring wrong value, SQLite busy errors, middleware rescue handling and UI refresh <!-- 33bd079 -->
992
1035
 
993
1036
  <!-- excluded:
994
- 4637391 pipeline only
995
- b53b4d7 pipeline only
996
- 7b33bf6 pipeline only
1037
+ 9da240b pipeline only
1038
+ 09748d1 pipeline only
1039
+ 4e8500b pipeline only
997
1040
  -->
998
1041
 
999
1042
  ## [0.19.0] - 2026-04-15
1000
1043
 
1001
1044
  ### Added
1002
1045
 
1003
- - **Env:** Propagate env var overrides to Sidekiq workers <!-- e75a883 -->
1046
+ - **Env:** Propagate env var overrides to Sidekiq workers <!-- c404799 -->
1004
1047
 
1005
1048
  ## [0.18.0] - 2026-04-14
1006
1049
 
1007
1050
  ### Changed
1008
1051
 
1009
- - **UI:** Extract shared tab utilities and redesign the Request tab <!-- 7aa8adb -->
1052
+ - **UI:** Extract shared tab utilities and redesign the Request tab <!-- 47e6269 -->
1010
1053
 
1011
1054
  ### Fixed
1012
1055
 
1013
- - **HTTP:** Capture outbound request body passed as second argument to `http.request()` <!-- 5a59665 -->
1056
+ - **HTTP:** Capture outbound request body passed as second argument to `http.request()` <!-- 2dda00f -->
1014
1057
 
1015
1058
  ## [0.17.0] - 2026-04-13
1016
1059
 
1017
1060
  ### Added
1018
1061
 
1019
- - **Dashboard:** Add env tab on the home page and env/job toolbar items <!-- d0277ab -->
1062
+ - **Dashboard:** Add env tab on the home page and env/job toolbar items <!-- c623c40 -->
1020
1063
 
1021
1064
  ## [0.16.0] - 2026-04-13
1022
1065
 
1023
1066
  ### Added
1024
1067
 
1025
- - **Function profiler:** Add `stackprof` sampling mode with multi-clock support and GC visibility <!-- 5f73512 -->
1068
+ - **Function profiler:** Add `stackprof` sampling mode with multi-clock support and GC visibility <!-- 5f461d9 -->
1026
1069
 
1027
1070
  ### Fixed
1028
1071
 
1029
- - **Function profiler:** Fix `stackprof` bugs, remove minimal mode, improve performance <!-- 76c6197 -->
1072
+ - **Function profiler:** Fix `stackprof` bugs, remove minimal mode, improve performance <!-- 8adc3ca -->
1030
1073
 
1031
1074
  ## [0.15.0] - 2026-04-11
1032
1075
 
1033
1076
  ### Added
1034
1077
 
1035
- - **Env:** Add environment variables tab <!-- 0e85f18 -->
1078
+ - **Env:** Add environment variables tab <!-- fa3bcec -->
1036
1079
 
1037
1080
  ## [0.14.0] - 2026-04-09
1038
1081
 
1039
1082
  ### Added
1040
1083
 
1041
- - **Profiler:** Link jobs to their triggering request or parent job <!-- 74ba5e3 -->
1084
+ - **Profiler:** Link jobs to their triggering request or parent job <!-- 5d598f6 -->
1042
1085
 
1043
1086
  ## [0.13.0] - 2026-04-09
1044
1087
 
1045
1088
  ### Added
1046
1089
 
1047
- - **UI:** Add copy/download buttons on HTTP bodies, waterfall view, date sort, and other UX improvements <!-- 5afd003 -->
1048
- - **UI:** Always show waterfall above the HTTP request list <!-- 32c6b6d -->
1049
- - **UI:** Use `SmartBodyPreview` in RequestTab for copy/download on request and response bodies <!-- aef4c0c -->
1090
+ - **UI:** Add copy/download buttons on HTTP bodies, waterfall view, date sort, and other UX improvements <!-- e7b430b -->
1091
+ - **UI:** Always show waterfall above the HTTP request list <!-- bec6cb0 -->
1092
+ - **UI:** Use `SmartBodyPreview` in RequestTab for copy/download on request and response bodies <!-- 439a9ac -->
1050
1093
 
1051
1094
  ### Fixed
1052
1095
 
1053
- - **UI:** Derive download file extension from actual MIME type instead of body category <!-- 1dc0bc8 -->
1054
- - **UI:** Move `sortedJobs` after `filteredJobs` to fix TDZ error <!-- d4de7bc -->
1096
+ - **UI:** Derive download file extension from actual MIME type instead of body category <!-- 987a65e -->
1097
+ - **UI:** Move `sortedJobs` after `filteredJobs` to fix TDZ error <!-- 235d492 -->
1055
1098
 
1056
1099
  ## [0.12.0] - 2026-04-09
1057
1100
 
1058
1101
  ### Added
1059
1102
 
1060
- - **Profiler:** Add function-level profiling with flamegraph and stats table <!-- fcb04a6 -->
1103
+ - **Profiler:** Add function-level profiling with flamegraph and stats table <!-- 2781cff -->
1061
1104
 
1062
1105
  ## [0.11.1] - 2026-04-09
1063
1106
 
1064
1107
  ### Added
1065
1108
 
1066
- - **Config:** Add `tmp_path` option to centralize the storage directory <!-- 5e77944 -->
1109
+ - **Config:** Add `tmp_path` option to centralize the storage directory <!-- d68a4c7 -->
1067
1110
 
1068
1111
  ### Fixed
1069
1112
 
1070
- - **MCP:** Decompress gzip+base64 bodies before returning to UI and MCP tool results <!-- 8f5ac47 -->
1113
+ - **MCP:** Decompress gzip+base64 bodies before returning to UI and MCP tool results <!-- c551773 -->
1071
1114
 
1072
1115
  ## [0.11.0] - 2026-04-06
1073
1116
 
1074
1117
  ### Added
1075
1118
 
1076
- - **UI:** Add load-more pagination to the profile list <!-- 9f8458b -->
1119
+ - **UI:** Add load-more pagination to the profile list <!-- ef49a2a -->
1077
1120
 
1078
1121
  ## [0.10.1] - 2026-04-06
1079
1122
 
1080
1123
  ### Changed
1081
1124
 
1082
- - **Docs:** Add full user documentation, with a UI guide, an MCP guide and screenshots <!-- 80a9198 -->
1125
+ - **Docs:** Add full user documentation, with a UI guide, an MCP guide and screenshots <!-- 2e34dc1 -->
1083
1126
 
1084
1127
  ## [0.10.0] - 2026-04-06
1085
1128
 
1086
1129
  ### Added
1087
1130
 
1088
- - **Middleware:** Add configurable CORS origins, CSP nonce support and body compression <!-- 3a42188 -->
1131
+ - **Middleware:** Add configurable CORS origins, CSP nonce support and body compression <!-- bb13049 -->
1089
1132
 
1090
1133
  ## [0.9.1] - 2026-04-06
1091
1134
 
1092
1135
  ### Removed
1093
1136
 
1094
- - **Collectors:** Remove `PerformanceCollector`, replaced by `FlameGraphCollector` <!-- 212e155 -->
1137
+ - **Collectors:** Remove `PerformanceCollector`, replaced by `FlameGraphCollector` <!-- fd03ac6 -->
1095
1138
 
1096
1139
  ## [0.9.0] - 2026-04-06
1097
1140
 
1098
1141
  ### Added
1099
1142
 
1100
- - **Profiler:** Add the `Profiler.measure` custom instrumentation API <!-- 0a3044e -->
1143
+ - **Profiler:** Add the `Profiler.measure` custom instrumentation API <!-- a3ab553 -->
1101
1144
 
1102
1145
  ## [0.8.0] - 2026-04-05
1103
1146
 
1104
1147
  ### Added
1105
1148
 
1106
- - **UI:** Add sortable columns, quick filter presets and flamegraph search <!-- ffd2e6f -->
1149
+ - **UI:** Add sortable columns, quick filter presets and flamegraph search <!-- 5bfb511 -->
1107
1150
 
1108
1151
  ## [0.7.0] - 2026-04-05
1109
1152
 
1110
1153
  ### Added
1111
1154
 
1112
- - **Database:** Add N+1 visual detection and `EXPLAIN ANALYZE` integration <!-- a22902d -->
1155
+ - **Database:** Add N+1 visual detection and `EXPLAIN ANALYZE` integration <!-- af266fc -->
1113
1156
 
1114
1157
  ## [0.6.0] - 2026-04-05
1115
1158
 
1116
1159
  ### Added
1117
1160
 
1118
- - **MCP:** Reduce token usage with filtering, body file saving and pagination <!-- 834251d -->
1161
+ - **MCP:** Reduce token usage with filtering, body file saving and pagination <!-- fa67dc8 -->
1119
1162
 
1120
1163
  ## [0.5.0] - 2026-04-04
1121
1164
 
1122
1165
  ### Added
1123
1166
 
1124
- - **MCP:** Add outbound IDs, a domain filter, datetimes, a latest token, exceptions and copy helpers <!-- 9330545 -->
1167
+ - **MCP:** Add outbound IDs, a domain filter, datetimes, a latest token, exceptions and copy helpers <!-- b36bc24 -->
1125
1168
 
1126
1169
  ## [0.4.0] - 2026-04-03
1127
1170
 
1128
1171
  ### Added
1129
1172
 
1130
- - **Collector:** Add an I18n panel tracking translation lookups per request <!-- 3032a15 -->
1173
+ - **Collector:** Add an I18n panel tracking translation lookups per request <!-- ef4a672 -->
1131
1174
 
1132
1175
  <!-- excluded:
1133
- b50d1eb pipeline only
1176
+ ad4fea1 pipeline only
1134
1177
  -->
1135
1178
 
1136
1179
  ## [0.3.0] - 2026-04-02
1137
1180
 
1138
1181
  ### Added
1139
1182
 
1140
- - **Collector:** Add a routes panel and enrich the request collector <!-- bdf0e3c -->
1183
+ - **Collector:** Add a routes panel and enrich the request collector <!-- febed69 -->
1141
1184
 
1142
1185
  ## [0.2.0] - 2026-03-29
1143
1186
 
1144
1187
  ### Added
1145
1188
 
1146
- - **Collector:** Add a Logs panel, capturing `Rails.logger` messages per request <!-- 0e0e7ce -->
1147
- - Publish the gem to RubyGems.org <!-- f292a21 -->
1189
+ - **Collector:** Add a Logs panel, capturing `Rails.logger` messages per request <!-- 3f1d906 -->
1190
+ - Publish the gem to RubyGems.org <!-- a63e6b4 -->
1148
1191
 
1149
1192
  <!-- excluded:
1150
- 10c1314 pipeline only, despite the fix: type
1151
- 3f97b1d pipeline only
1152
- a8c2044 pipeline only
1153
- 27cc550 pipeline only
1193
+ 17feea9 pipeline only, despite the fix: type
1194
+ 86fcb3f pipeline only
1195
+ 5891bfa pipeline only
1196
+ 81150a3 pipeline only
1154
1197
  -->
1155
1198
 
1156
1199
  ## [0.1.4] - 2026-03-29
1157
1200
 
1158
1201
  ### Added
1159
1202
 
1160
- - **Profiler:** Capture the full request and response, and add an MCP history clear <!-- 9922c8a -->
1203
+ - **Profiler:** Capture the full request and response, and add an MCP history clear <!-- 7d54262 -->
1161
1204
 
1162
1205
  ## [0.1.3] - 2026-03-29
1163
1206
 
1164
1207
  ### Changed
1165
1208
 
1166
- - **Assets:** Migrate Sass `@import` to `@use`, which removes the build deprecation warnings <!-- 1a67e48 -->
1209
+ - **Assets:** Migrate Sass `@import` to `@use`, which removes the build deprecation warnings <!-- a0ac9b5 -->
1167
1210
 
1168
1211
  ## [0.1.2] - 2026-03-25
1169
1212
 
@@ -1204,7 +1247,7 @@ carries no change to the gem, so there is nothing to report for this version.
1204
1247
  - **Profiler:** Improve memory tracking and profile deserialisation <!-- a0af47e -->
1205
1248
 
1206
1249
  <!-- excluded:
1207
- e27cdae pipeline only
1250
+ d797be5 pipeline only
1208
1251
  02d63c9 pipeline only
1209
1252
  9f598d9 subject is not conventional, tooling only
1210
1253
  7d1d823 tests only
@@ -3723,7 +3723,23 @@
3723
3723
  }
3724
3724
 
3725
3725
  // app/assets/typescript/profiler/components/toolbar/panels/JobsPanel.tsx
3726
+ function JobStatus({ job }) {
3727
+ if (job.status === "running") {
3728
+ return /* @__PURE__ */ u3("strong", { class: "profiler-text--xs profiler-text--mono profiler-text--info", children: [
3729
+ /* @__PURE__ */ u3("span", { class: "profiler-spinner", "aria-hidden": "true", children: "\u25D0" }),
3730
+ " running"
3731
+ ] });
3732
+ }
3733
+ const failed = job.status === "failed";
3734
+ return /* @__PURE__ */ u3("strong", { class: `profiler-text--xs profiler-text--mono ${failed ? "profiler-text--error" : "profiler-text--success"}`, children: [
3735
+ failed ? "\u2717" : "\u2713",
3736
+ " ",
3737
+ job.duration?.toFixed(0),
3738
+ "ms"
3739
+ ] });
3740
+ }
3726
3741
  function JobsPanel({ jobs }) {
3742
+ const running = jobs.filter((j3) => j3.status === "running").length;
3727
3743
  const completed = jobs.filter((j3) => j3.status === "completed").length;
3728
3744
  const failed = jobs.filter((j3) => j3.status === "failed").length;
3729
3745
  const preview = jobs.slice(0, 5);
@@ -3739,9 +3755,13 @@
3739
3755
  ] }),
3740
3756
  /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-content", children: [
3741
3757
  /* @__PURE__ */ u3("div", { class: "profiler-section__header", children: "Summary" }),
3758
+ running > 0 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
3759
+ /* @__PURE__ */ u3("span", { children: "Running" }),
3760
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--info", children: running })
3761
+ ] }),
3742
3762
  /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
3743
3763
  /* @__PURE__ */ u3("span", { children: "Completed" }),
3744
- /* @__PURE__ */ u3("strong", { class: "profiler-text--success", children: completed })
3764
+ /* @__PURE__ */ u3("strong", { class: completed > 0 ? "profiler-text--success" : "profiler-text--muted", children: completed })
3745
3765
  ] }),
3746
3766
  /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
3747
3767
  /* @__PURE__ */ u3("span", { children: "Failed" }),
@@ -3750,12 +3770,7 @@
3750
3770
  /* @__PURE__ */ u3("div", { class: "profiler-section__header profiler-mt-3", children: "Last jobs" }),
3751
3771
  preview.map((job, i3) => /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
3752
3772
  /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: job.job_class }),
3753
- /* @__PURE__ */ u3("strong", { class: `profiler-text--xs profiler-text--mono ${job.status === "failed" ? "profiler-text--error" : "profiler-text--success"}`, children: [
3754
- job.status === "failed" ? "\u2717" : "\u2713",
3755
- " ",
3756
- job.duration?.toFixed(0),
3757
- "ms"
3758
- ] })
3773
+ /* @__PURE__ */ u3(JobStatus, { job })
3759
3774
  ] }, i3)),
3760
3775
  remaining > 0 && /* @__PURE__ */ u3("div", { class: "profiler-more", children: [
3761
3776
  "+ ",
@@ -4102,13 +4117,14 @@
4102
4117
  ToolbarItem,
4103
4118
  {
4104
4119
  href: `/_profiler/profiles/${token}?tab=jobs`,
4105
- className: childJobs.some((j3) => j3.status === "failed") ? "profiler-text--error" : "profiler-text--success",
4120
+ className: childJobs.some((j3) => j3.status === "failed") ? "profiler-text--error" : childJobs.some((j3) => j3.status === "running") ? "profiler-text--info" : "profiler-text--success",
4106
4121
  panelLarge: true,
4107
4122
  panel: /* @__PURE__ */ u3(JobsPanel, { jobs: childJobs }),
4108
4123
  children: [
4109
4124
  /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "JOB" }),
4110
4125
  /* @__PURE__ */ u3("span", { children: childJobs.length }),
4111
- childJobs.some((j3) => j3.status === "failed") && /* @__PURE__ */ u3("span", { class: "profiler-text--error profiler-text--xs", children: "\u2717" })
4126
+ childJobs.some((j3) => j3.status === "failed") && /* @__PURE__ */ u3("span", { class: "profiler-text--error profiler-text--xs", children: "\u2717" }),
4127
+ childJobs.some((j3) => j3.status === "running") && /* @__PURE__ */ u3("span", { class: "profiler-spinner profiler-text--info profiler-text--xs", "aria-hidden": "true", children: "\u25D0" })
4112
4128
  ]
4113
4129
  }
4114
4130
  ),
@@ -606,6 +606,11 @@ tr:hover .btn-row-delete {
606
606
  transform: rotate(360deg);
607
607
  }
608
608
  }
609
+ .profiler-spinner {
610
+ display: inline-block;
611
+ animation: spin 1s linear infinite;
612
+ }
613
+
609
614
  @keyframes pulse {
610
615
  0%, 100% {
611
616
  opacity: 1;
@@ -1558,6 +1563,10 @@ a.profiler-toolbar-item {
1558
1563
  color: var(--profiler-error) !important;
1559
1564
  }
1560
1565
 
1566
+ .profiler-text--info {
1567
+ color: var(--profiler-info) !important;
1568
+ }
1569
+
1561
1570
  .profiler-text--muted {
1562
1571
  color: var(--profiler-text-muted) !important;
1563
1572
  }
@@ -1578,6 +1587,10 @@ a.profiler-toolbar-item {
1578
1587
  background: var(--profiler-success);
1579
1588
  }
1580
1589
 
1590
+ .profiler-toolbar-item.profiler-text--info::after {
1591
+ background: var(--profiler-info);
1592
+ }
1593
+
1581
1594
  .profiler-toolbar-hoverable {
1582
1595
  position: relative;
1583
1596
  }
@@ -5569,6 +5569,11 @@
5569
5569
  if (duration >= 100) return "badge-warning";
5570
5570
  return "badge-success";
5571
5571
  }
5572
+ function jobStatusOf(p3) {
5573
+ const status = p3.collectors_data?.job?.status;
5574
+ if (status) return status;
5575
+ return p3.status === 500 ? "failed" : "completed";
5576
+ }
5572
5577
  function formatTime(iso) {
5573
5578
  return new Date(iso).toLocaleTimeString("en", { hour12: false });
5574
5579
  }
@@ -5827,8 +5832,7 @@
5827
5832
  }) : filteredProfiles;
5828
5833
  const filteredJobs = jobs.filter((p3) => {
5829
5834
  if (jobSearch && !p3.path.toLowerCase().includes(jobSearch.toLowerCase())) return false;
5830
- if (jobStatus === "failed" && p3.status !== 500) return false;
5831
- if (jobStatus === "completed" && p3.status === 500) return false;
5835
+ if (jobStatus && jobStatusOf(p3) !== jobStatus) return false;
5832
5836
  if (jobDuration) {
5833
5837
  if (jobDuration === "lt100" ? p3.duration >= 100 : p3.duration < parseInt(jobDuration)) return false;
5834
5838
  }
@@ -6104,6 +6108,7 @@
6104
6108
  ),
6105
6109
  /* @__PURE__ */ u3("select", { class: "profiler-filter-select", value: jobStatus, onChange: (e3) => setJobStatus(e3.target.value), children: [
6106
6110
  /* @__PURE__ */ u3("option", { value: "", children: "All Statuses" }),
6111
+ /* @__PURE__ */ u3("option", { value: "running", children: "Running" }),
6107
6112
  /* @__PURE__ */ u3("option", { value: "completed", children: "Completed" }),
6108
6113
  /* @__PURE__ */ u3("option", { value: "failed", children: "Failed" })
6109
6114
  ] }),
@@ -6147,7 +6152,7 @@
6147
6152
  ] }) }),
6148
6153
  /* @__PURE__ */ u3("tbody", { children: sortedJobs.map((p3) => {
6149
6154
  const jobData = p3.collectors_data?.job;
6150
- const isFailed = p3.status === 500;
6155
+ const status = jobStatusOf(p3);
6151
6156
  return /* @__PURE__ */ u3("tr", { children: [
6152
6157
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
6153
6158
  /* @__PURE__ */ u3("td", { children: [
@@ -6155,11 +6160,14 @@
6155
6160
  p3.gem_version && p3.gem_version !== currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-warn", title: `Captur\xE9 avec v${p3.gem_version} (actuel : v${currentVersion})`, children: "\u26A0\uFE0F" })
6156
6161
  ] }),
6157
6162
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: jobData?.queue || "-" }) }),
6158
- /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
6163
+ /* @__PURE__ */ u3("td", { children: status === "running" ? /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: "-" }) : /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
6159
6164
  p3.duration.toFixed(2),
6160
6165
  " ms"
6161
6166
  ] }) }),
6162
- /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: isFailed ? "badge-error" : "badge-success", children: isFailed ? "\u2717 Failed" : "\u2713 Completed" }) }),
6167
+ /* @__PURE__ */ u3("td", { children: status === "running" ? /* @__PURE__ */ u3("span", { class: "badge-info", children: [
6168
+ /* @__PURE__ */ u3("span", { class: "profiler-spinner", "aria-hidden": "true", children: "\u25D0" }),
6169
+ " Running"
6170
+ ] }) : /* @__PURE__ */ u3("span", { class: status === "failed" ? "badge-error" : "badge-success", children: status === "failed" ? "\u2717 Failed" : "\u2713 Completed" }) }),
6163
6171
  /* @__PURE__ */ u3("td", { children: jobData?.executions ?? "-" }),
6164
6172
  /* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
6165
6173
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteJob2(p3.token), title: "Delete", children: "\xD7" }) })
@@ -8631,10 +8639,10 @@
8631
8639
  jobs.map((job, index) => /* @__PURE__ */ u3("div", { class: `profiler-ajax-card profiler-ajax-card--${job.status === "completed" ? "success" : job.status === "failed" ? "error" : "default"}`, children: [
8632
8640
  /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
8633
8641
  /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", children: [
8634
- /* @__PURE__ */ u3("span", { class: `badge-${job.status === "completed" ? "success" : job.status === "failed" ? "error" : "warning"}`, children: job.status ?? "unknown" }),
8642
+ /* @__PURE__ */ u3("span", { class: `badge-${job.status === "completed" ? "success" : job.status === "failed" ? "error" : job.status === "running" ? "info" : "warning"}`, children: job.status ?? "unknown" }),
8635
8643
  /* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", children: job.job_class })
8636
8644
  ] }),
8637
- /* @__PURE__ */ u3("span", { class: job.duration >= 1e3 ? "badge-error" : job.duration >= 200 ? "badge-warning" : "badge-success", children: [
8645
+ job.status === "running" ? /* @__PURE__ */ u3("span", { class: "badge-info", children: "running" }) : /* @__PURE__ */ u3("span", { class: job.duration >= 1e3 ? "badge-error" : job.duration >= 200 ? "badge-warning" : "badge-success", children: [
8638
8646
  job.duration?.toFixed(2),
8639
8647
  " ms"
8640
8648
  ] })
@@ -9063,6 +9071,7 @@
9063
9071
  const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
9064
9072
  const jobData = cd["job"];
9065
9073
  const isFailed = jobData?.status === "failed";
9074
+ const isRunning = jobData?.status === "running";
9066
9075
  const parent = profile.parent_profile;
9067
9076
  const handleTabClick = (tab) => (e3) => {
9068
9077
  e3.preventDefault();
@@ -9082,14 +9091,11 @@
9082
9091
  /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mt-2", children: [
9083
9092
  /* @__PURE__ */ u3("span", { children: [
9084
9093
  "Duration: ",
9085
- /* @__PURE__ */ u3("strong", { children: [
9086
- profile.duration.toFixed(2),
9087
- " ms"
9088
- ] })
9094
+ /* @__PURE__ */ u3("strong", { children: isRunning ? "running" : `${profile.duration.toFixed(2)} ms` })
9089
9095
  ] }),
9090
9096
  /* @__PURE__ */ u3("span", { children: [
9091
9097
  "Status: ",
9092
- /* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : "success"}`, children: isFailed ? "Failed" : "Completed" }) })
9098
+ /* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : isRunning ? "info" : "success"}`, children: isFailed ? "Failed" : isRunning ? "Running" : "Completed" }) })
9093
9099
  ] }),
9094
9100
  allocatedObjects(profile) != null && /* @__PURE__ */ u3("span", { title: ALLOCATIONS_HINT, children: [
9095
9101
  "Allocations: ",
@@ -72,6 +72,7 @@ module Profiler
72
72
  return block.call unless Collectors::Lifecycle.subscribe_all(collectors, "JobProfiler")
73
73
 
74
74
  exception_collector = collectors.find { |c| c.is_a?(Collectors::ExceptionCollector) }
75
+ save_running(profile, job_collector) if @parent_token
75
76
 
76
77
  allocations_before = AllocationCounter.current if Profiler.configuration.track_memory
77
78
 
@@ -118,6 +119,16 @@ module Profiler
118
119
 
119
120
  private
120
121
 
122
+ # A job enqueued by a request runs after that request's response: saved now, running, the
123
+ # toolbar of the request lists it before it ends. The save at the end replaces it.
124
+ def save_running(profile, job_collector)
125
+ job_collector.collect
126
+ profile.duration = 0.0
127
+ Profiler.save_profile(profile, from: "JobProfiler")
128
+ rescue StandardError => e
129
+ Profiler.log_error("JobProfiler: could not record job #{@job_class} as running", e)
130
+ end
131
+
121
132
  def sanitize_arguments(args)
122
133
  return [] unless args
123
134
 
@@ -230,7 +230,7 @@ module Profiler
230
230
  input_schema: {
231
231
  properties: {
232
232
  queue: { type: "string", description: "Filter by queue name" },
233
- status: { type: "string", description: "Filter by status (completed, failed)" },
233
+ status: { type: "string", description: "Filter by status (running, completed, failed). A job enqueued by a request is listed as running while it performs." },
234
234
  limit: { type: "number", description: "Maximum number of results (default 20)" },
235
235
  fields: { type: "array", items: { type: "string" }, description: "Columns to include. Valid values: time, job_class, queue, status, duration, token. Omit for all." },
236
236
  cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen. Returns jobs older than this." },
@@ -233,6 +233,8 @@ module Profiler
233
233
  from { transform: translateY(100%); opacity: 0; }
234
234
  to { transform: translateY(0); opacity: 1; }
235
235
  }
236
+ @keyframes pfSpin { to { transform: rotate(360deg); } }
237
+ #profiler-toolbar .profiler-spinner { display: inline-block; animation: pfSpin 1s linear infinite; }
236
238
  @keyframes pfPop {
237
239
  0% { transform: scaleX(1); }
238
240
  15% { transform: scaleX(1.18); }
@@ -303,11 +305,13 @@ module Profiler
303
305
  #profiler-toolbar .profiler-text--error::after { background: var(--pf-error); }
304
306
  #profiler-toolbar .profiler-text--warning::after { background: var(--pf-warning); }
305
307
  #profiler-toolbar .profiler-text--success::after { background: var(--pf-success); }
308
+ #profiler-toolbar .profiler-text--info::after { background: var(--pf-info); }
306
309
 
307
310
  /* Semantic colors */
308
311
  #profiler-toolbar .profiler-text--success { color: var(--pf-success) !important; }
309
312
  #profiler-toolbar .profiler-text--warning { color: var(--pf-warning) !important; }
310
313
  #profiler-toolbar .profiler-text--error { color: var(--pf-error) !important; }
314
+ #profiler-toolbar .profiler-text--info { color: var(--pf-info) !important; }
311
315
  #profiler-toolbar .profiler-text--accent { color: var(--pf-amber) !important; }
312
316
  #profiler-toolbar .profiler-text--muted { color: var(--pf-muted) !important; }
313
317
 
@@ -6,13 +6,15 @@ module Profiler
6
6
  module Storage
7
7
  class BaseStore
8
8
  # Public interface: persists the profile then records the save, for the pages showing it
9
- # to see it changed (Profiler::SSE).
9
+ # to see it changed (Profiler::SSE). A job's save is also recorded for the profile that
10
+ # enqueued it: the toolbar of that request lists its jobs, and has to see them start and end.
10
11
  # Subclasses implement #do_save, not #save. The token must be one the gem issued
11
12
  # (Storage::Token); every other public method answers "not found" for any other token.
12
13
  def save(token, profile)
13
14
  Token.validate!(token)
14
15
  result = do_save(token, profile)
15
16
  broadcast_event(token)
17
+ broadcast_event(profile.parent_token) if job_with_parent?(profile)
16
18
  result
17
19
  end
18
20
 
@@ -53,6 +55,11 @@ module Profiler
53
55
 
54
56
  private
55
57
 
58
+ # Not for an AJAX request linked to its page: the toolbar reads those on its own links.
59
+ def job_with_parent?(profile)
60
+ profile.profile_type == "job" && Token.valid?(profile.parent_token)
61
+ end
62
+
56
63
  def broadcast_event(token)
57
64
  Profiler::SSE.current.broadcast(token)
58
65
  rescue StandardError => e
@@ -24,14 +24,32 @@ module Profiler
24
24
 
25
25
  module RunWrapper
26
26
  def run
27
+ file, line = profiler_test_location
27
28
  Profiler::TestProfiler.profile(
28
29
  test_name: "#{self.class.name}##{name}",
29
- test_file: self.class.instance_method(name).source_location&.first || "",
30
- test_line: self.class.instance_method(name).source_location&.last || 0,
30
+ test_file: file,
31
+ test_line: line,
31
32
  framework: :minitest
32
33
  ) { super }
34
+ rescue StandardError => e
35
+ # Test#run captures what the test raises, so this comes from the
36
+ # profiler. Escaping, it would stop the whole run: it fails this
37
+ # test instead, as it does an RSpec example, without running it again.
38
+ self.time ||= 0
39
+ capture_exceptions { raise e }
40
+ Minitest::Result.from(self)
41
+ end
42
+
43
+ private
44
+
45
+ # Only the lookup is guarded: rescuing around super would run the test
46
+ # again whenever anything raised a NameError after it, NoMethodError
47
+ # included.
48
+ def profiler_test_location
49
+ location = self.class.instance_method(name).source_location
50
+ [location&.first || "", location&.last || 0]
33
51
  rescue NameError
34
- super
52
+ ["", 0]
35
53
  end
36
54
  end
37
55
  end
@@ -15,19 +15,70 @@ module Profiler
15
15
  # end
16
16
  module RSpecSupport
17
17
  def self.install(config)
18
- config.around(:each) do |example|
18
+ hook = proc do |example|
19
19
  Profiler::TestProfiler.profile(
20
20
  test_name: example.full_description,
21
21
  test_file: example.file_path,
22
- test_line: example.line_number,
22
+ test_line: example.metadata[:line_number],
23
23
  framework: :rspec
24
- ) { example.run }
24
+ ) do
25
+ example.run
26
+ Outcome.new(example)
27
+ end
28
+ end
29
+
30
+ # config.around would nest this hook inside the config-level around
31
+ # hooks registered before it, among them the one RSpec registers for
32
+ # :aggregate_failures when the configuration is created. An aggregated
33
+ # failure, like one raised by such a hook after the example, is only
34
+ # set on the example once that hook has returned: the example would be
35
+ # recorded as passed. Appended, the hook wraps them all. Groups already
36
+ # defined get it the way config.around gives it to them. config.hooks
37
+ # is not public API, hence the fallback.
38
+ if config.respond_to?(:hooks) && config.hooks.respond_to?(:register)
39
+ config.hooks.register(:append, :around, :example, &hook)
40
+ RSpec.world.example_groups.each do |group|
41
+ group.hooks.register(:append, :around, :example, &hook)
42
+ end
43
+ else
44
+ config.around(:example, &hook)
25
45
  end
26
46
 
27
47
  config.after(:suite) do
28
48
  Profiler::TestHelpers::Reporter.print
29
49
  end
30
50
  end
51
+
52
+ # What the profiled block hands back to TestProfiler: the same interface as
53
+ # Minitest::Result (passed?, skipped?, failure).
54
+ #
55
+ # Procsy#run returns a boolean, and execution_result.status is still nil at
56
+ # this point: Example#finish only writes it once every around hook has
57
+ # returned. The outcome is read from the state #finish relies on, in the
58
+ # same order: an exception means failed, then a pending message (from
59
+ # `pending` or `skip` in the example) means pending.
60
+ class Outcome
61
+ Reason = Struct.new(:message)
62
+
63
+ def initialize(example)
64
+ @exception = example.exception
65
+ @pending_message = example.execution_result.pending_message
66
+ end
67
+
68
+ def passed?
69
+ @exception.nil? && @pending_message.nil?
70
+ end
71
+
72
+ def skipped?
73
+ @exception.nil? && !@pending_message.nil?
74
+ end
75
+
76
+ def failure
77
+ return @exception if @exception
78
+
79
+ Reason.new(@pending_message) if @pending_message
80
+ end
81
+ end
31
82
  end
32
83
  end
33
84
  end
@@ -67,8 +67,10 @@ module Profiler
67
67
  begin
68
68
  result = block.call
69
69
 
70
- # Minitest: Test#run catches assertion errors internally and returns self.
71
- # Failures don't propagate — detect them from the result object.
70
+ # Test frameworks catch failures themselves, so they never reach the
71
+ # rescue below. The block reports them by returning an object that
72
+ # responds to passed?, skipped? and failure: Minitest::Test#run returns
73
+ # a Minitest::Result, and RSpecSupport builds an equivalent outcome.
72
74
  if result.respond_to?(:passed?)
73
75
  unless result.passed?
74
76
  test_status = result.skipped? ? "pending" : "failed"
@@ -81,17 +83,6 @@ module Profiler
81
83
  )
82
84
  end
83
85
 
84
- # RSpec: example.run catches errors internally and stores them in execution_result.
85
- if result.respond_to?(:execution_result)
86
- er = result.execution_result
87
- rspec_status = er.status&.to_s
88
- if rspec_status && rspec_status != "passed" && test_status == "passed"
89
- test_status = rspec_status
90
- msg = er.exception&.message.to_s
91
- error_message = msg.empty? ? nil : msg
92
- end
93
- end
94
-
95
86
  result
96
87
  rescue Exception => e # rubocop:disable Lint/RescueException
97
88
  # Capture all exceptions (including test failures which may subclass Exception)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.32.1"
4
+ VERSION = "0.32.3"
5
5
  end
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.32.1
4
+ version: 0.32.3
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-10-13 00:00:00.000000000 Z
11
+ date: 2026-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails