rails-profiler 0.17.0 → 0.19.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: af1361f732061dbeaa22f808fb18e31d812e570eac67a7dd08687257d1400399
4
- data.tar.gz: 0603edc83c99eb5122896e7aeb07a7cff85e944acfc45a3fc1268eae2b4e69aa
3
+ metadata.gz: a4e5e84c4946f549552450c0d2d2d2053b273d3bed8dba66fefa17d9c4e5f7f9
4
+ data.tar.gz: 6b4be4f2b3116dbdc4270df5a6f860eabbbf709c3fc296ea34f400aaeb26ce83
5
5
  SHA512:
6
- metadata.gz: 5e615b88c8d7253e24e00b416b1e5b6b858627a629714a6f041c77673fa1be1aee23bc57b999fc5016b101263c5fbc510594d6300994c007ccdef62ddab2fe42
7
- data.tar.gz: 2cbe8336230ebd48a49b706a82d3f898516841b2b93d424a74a4f0aa9d9f23ab917e75ea91aef70a238f3c79db3460d3974765eda260e71efbee1864229b3f30
6
+ metadata.gz: 66136505b84f792418deb7a29d7d77f36a3cc62cf3bdab9d7defc842f9797b39667c92e18984925a14276880a129ef5b42faa644112ab1c917b61e9f11f8b359
7
+ data.tar.gz: b4ac6c0b37496721a3960732ab0839224e896aa4390757b36d84595200e6441034649f8829573a1fee30cf9c8216c5275e5faa9b0534bc48fe49a20e738d2bf3
@@ -396,6 +396,24 @@
396
396
  return "function" == typeof t3 ? t3(n2) : t3;
397
397
  }
398
398
 
399
+ // app/assets/typescript/profiler/components/dashboard/tabs/shared/utils.ts
400
+ function methodBadge(method) {
401
+ const map = { GET: "info", POST: "success", PUT: "warning", PATCH: "warning", DELETE: "error" };
402
+ return map[method] || "default";
403
+ }
404
+ function statusBadge(status) {
405
+ if (status === 0) return "error";
406
+ if (status >= 200 && status < 300) return "success";
407
+ if (status >= 400) return "error";
408
+ return "warning";
409
+ }
410
+ function formatBytes(bytes) {
411
+ if (bytes < 0) return "\u2014";
412
+ if (bytes < 1024) return `${bytes} B`;
413
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
414
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
415
+ }
416
+
399
417
  // node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js
400
418
  var f3 = 0;
401
419
  function u3(e3, t3, n2, o3, i3, u4) {
@@ -407,21 +425,53 @@
407
425
  return l.vnode && l.vnode(l3), l3;
408
426
  }
409
427
 
410
- // app/assets/typescript/profiler/components/dashboard/tabs/HttpTab.tsx
411
- function methodBadge(method) {
412
- const map = { GET: "info", POST: "success", PUT: "warning", PATCH: "warning", DELETE: "error" };
413
- return map[method] || "default";
428
+ // app/assets/typescript/profiler/components/dashboard/tabs/shared/HttpComponents.tsx
429
+ function CopyButton({ text }) {
430
+ const [copied, setCopied] = d2(false);
431
+ function copy() {
432
+ navigator.clipboard.writeText(text).then(() => {
433
+ setCopied(true);
434
+ setTimeout(() => setCopied(false), 2e3);
435
+ });
436
+ }
437
+ return /* @__PURE__ */ u3("button", { onClick: copy, class: "profiler-body-download-btn profiler-text--xs", style: "cursor:pointer", children: copied ? "Copied!" : "Copy" });
414
438
  }
415
- function statusBadge(status) {
416
- if (status === 0) return "error";
417
- if (status >= 200 && status < 300) return "success";
418
- if (status >= 400) return "error";
419
- return "warning";
439
+ function mimeToExt(mime) {
440
+ const m3 = mime.split(";")[0].trim().toLowerCase();
441
+ const map = {
442
+ "application/json": ".json",
443
+ "application/ld+json": ".jsonld",
444
+ "application/xml": ".xml",
445
+ "text/xml": ".xml",
446
+ "text/csv": ".csv",
447
+ "application/csv": ".csv",
448
+ "text/html": ".html",
449
+ "text/plain": ".txt",
450
+ "text/css": ".css",
451
+ "application/javascript": ".js",
452
+ "text/javascript": ".js",
453
+ "image/svg+xml": ".svg",
454
+ "image/png": ".png",
455
+ "image/jpeg": ".jpg",
456
+ "image/gif": ".gif",
457
+ "image/webp": ".webp",
458
+ "image/avif": ".avif",
459
+ "application/pdf": ".pdf",
460
+ "application/zip": ".zip",
461
+ "application/gzip": ".gz",
462
+ "application/octet-stream": ".bin"
463
+ };
464
+ return map[m3] || ".bin";
420
465
  }
421
- function formatBytes(n2) {
422
- if (n2 === 0) return "0 B";
423
- if (n2 < 1024) return `${n2} B`;
424
- return `${(n2 / 1024).toFixed(1)} KB`;
466
+ function DownloadTextButton({ text, mime }) {
467
+ const [url, setUrl] = d2(null);
468
+ y2(() => {
469
+ const objectUrl = URL.createObjectURL(new Blob([text], { type: mime }));
470
+ setUrl(objectUrl);
471
+ return () => URL.revokeObjectURL(objectUrl);
472
+ }, [text, mime]);
473
+ if (!url) return null;
474
+ return /* @__PURE__ */ u3("a", { href: url, download: `body${mimeToExt(mime)}`, class: "profiler-body-download-btn profiler-text--xs", children: "Download" });
425
475
  }
426
476
  function HeadersTable({ headers }) {
427
477
  const entries = Object.entries(headers);
@@ -494,8 +544,6 @@
494
544
  if (category === "xml") return formatXml(body);
495
545
  return body;
496
546
  }
497
- var PREVIEW_LIMIT = 500;
498
- var CSV_ROW_LIMIT = 10;
499
547
  function categoryMime(category) {
500
548
  const map = {
501
549
  json: "application/json",
@@ -506,53 +554,8 @@
506
554
  };
507
555
  return map[category] || "text/plain";
508
556
  }
509
- function mimeToExt(mime) {
510
- const m3 = mime.split(";")[0].trim().toLowerCase();
511
- const map = {
512
- "application/json": ".json",
513
- "application/ld+json": ".jsonld",
514
- "application/xml": ".xml",
515
- "text/xml": ".xml",
516
- "text/csv": ".csv",
517
- "application/csv": ".csv",
518
- "text/html": ".html",
519
- "text/plain": ".txt",
520
- "text/css": ".css",
521
- "application/javascript": ".js",
522
- "text/javascript": ".js",
523
- "image/svg+xml": ".svg",
524
- "image/png": ".png",
525
- "image/jpeg": ".jpg",
526
- "image/gif": ".gif",
527
- "image/webp": ".webp",
528
- "image/avif": ".avif",
529
- "application/pdf": ".pdf",
530
- "application/zip": ".zip",
531
- "application/gzip": ".gz",
532
- "application/octet-stream": ".bin"
533
- };
534
- return map[m3] || ".bin";
535
- }
536
- function CopyButton({ text }) {
537
- const [copied, setCopied] = d2(false);
538
- function copy() {
539
- navigator.clipboard.writeText(text).then(() => {
540
- setCopied(true);
541
- setTimeout(() => setCopied(false), 2e3);
542
- });
543
- }
544
- return /* @__PURE__ */ u3("button", { onClick: copy, class: "profiler-body-download-btn profiler-text--xs", style: "cursor:pointer", children: copied ? "Copied!" : "Copy" });
545
- }
546
- function DownloadTextButton({ text, mime }) {
547
- const [url, setUrl] = d2(null);
548
- y2(() => {
549
- const objectUrl = URL.createObjectURL(new Blob([text], { type: mime }));
550
- setUrl(objectUrl);
551
- return () => URL.revokeObjectURL(objectUrl);
552
- }, [text, mime]);
553
- if (!url) return null;
554
- return /* @__PURE__ */ u3("a", { href: url, download: `body${mimeToExt(mime)}`, class: "profiler-body-download-btn profiler-text--xs", children: "Download" });
555
- }
557
+ var PREVIEW_LIMIT = 500;
558
+ var CSV_ROW_LIMIT = 10;
556
559
  function SmartBodyPreview({ body, encoding, headers }) {
557
560
  const [expanded, setExpanded] = d2(false);
558
561
  const [objectUrl, setObjectUrl] = d2(null);
@@ -611,7 +614,7 @@
611
614
  )
612
615
  ] });
613
616
  }
614
- const formatted = formatTextBody(body, category) ?? body;
617
+ const formatted = formatTextBody(body, category);
615
618
  const preview = expanded ? formatted : formatted.slice(0, PREVIEW_LIMIT);
616
619
  const truncated = formatted.length > PREVIEW_LIMIT && !expanded;
617
620
  return /* @__PURE__ */ u3("div", { children: [
@@ -634,6 +637,98 @@
634
637
  )
635
638
  ] });
636
639
  }
640
+ function HttpReqRespDetail({ request, response, backtrace }) {
641
+ return /* @__PURE__ */ u3("div", { style: "padding:12px 4px 4px;border-top:1px solid rgba(0,0,0,0.08);margin-top:8px", children: [
642
+ /* @__PURE__ */ u3("div", { style: "margin-bottom:16px", children: [
643
+ /* @__PURE__ */ u3("div", { class: "profiler-text--sm", style: "font-weight:600;margin-bottom:8px;color:var(--profiler-muted)", children: "REQUEST" }),
644
+ /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Headers" }),
645
+ /* @__PURE__ */ u3(HeadersTable, { headers: request.headers }),
646
+ request.params && Object.keys(request.params).length > 0 && !request.body && /* @__PURE__ */ u3(k, { children: [
647
+ /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-top:10px;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Params" }),
648
+ /* @__PURE__ */ u3("pre", { class: "profiler-code profiler-text--xs", style: "margin:0", children: JSON.stringify(request.params, null, 2) })
649
+ ] }),
650
+ request.body && /* @__PURE__ */ u3(k, { children: [
651
+ /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-top:10px;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Body" }),
652
+ /* @__PURE__ */ u3(SmartBodyPreview, { body: request.body, encoding: request.body_encoding, headers: request.headers })
653
+ ] })
654
+ ] }),
655
+ /* @__PURE__ */ u3("div", { children: [
656
+ /* @__PURE__ */ u3("div", { class: "profiler-text--sm", style: "font-weight:600;margin-bottom:8px;color:var(--profiler-muted)", children: "RESPONSE" }),
657
+ /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Headers" }),
658
+ /* @__PURE__ */ u3(HeadersTable, { headers: response.headers }),
659
+ response.body && /* @__PURE__ */ u3(k, { children: [
660
+ /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-top:10px;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Body" }),
661
+ /* @__PURE__ */ u3(SmartBodyPreview, { body: response.body, encoding: response.body_encoding, headers: response.headers })
662
+ ] })
663
+ ] }),
664
+ backtrace && backtrace.length > 1 && /* @__PURE__ */ u3("div", { style: "margin-top:12px", children: [
665
+ /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Backtrace" }),
666
+ backtrace.map((frame, i3) => /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "padding:2px 0", children: frame }, i3))
667
+ ] })
668
+ ] });
669
+ }
670
+ function HttpCardHeader({ method, url, copyUrl: copyUrlOverride, status, duration, curlCommand, expandable, open, onToggle }) {
671
+ const [copiedUrl, setCopiedUrl] = d2(false);
672
+ const [copiedCurl, setCopiedCurl] = d2(false);
673
+ function handleCopyUrl(e3) {
674
+ e3.stopPropagation();
675
+ navigator.clipboard.writeText(copyUrlOverride ?? url).then(() => {
676
+ setCopiedUrl(true);
677
+ setTimeout(() => setCopiedUrl(false), 2e3);
678
+ });
679
+ }
680
+ function handleCopyCurl(e3) {
681
+ e3.stopPropagation();
682
+ navigator.clipboard.writeText(curlCommand).then(() => {
683
+ setCopiedCurl(true);
684
+ setTimeout(() => setCopiedCurl(false), 2e3);
685
+ });
686
+ }
687
+ return /* @__PURE__ */ u3(
688
+ "div",
689
+ {
690
+ class: "profiler-ajax-card__row",
691
+ style: expandable ? "cursor:pointer;user-select:none" : void 0,
692
+ onClick: expandable ? onToggle : void 0,
693
+ children: [
694
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", style: "min-width:0;flex:1", children: [
695
+ expandable && /* @__PURE__ */ u3("span", { style: "font-size:11px;color:var(--profiler-muted)", children: open ? "\u25BE" : "\u25B8" }),
696
+ /* @__PURE__ */ u3("span", { class: `profiler-ajax-card__method badge-${methodBadge(method)}`, children: method }),
697
+ /* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", style: "word-break:break-all", children: url }),
698
+ /* @__PURE__ */ u3(
699
+ "button",
700
+ {
701
+ onClick: handleCopyUrl,
702
+ class: "profiler-body-download-btn profiler-text--xs",
703
+ style: "flex-shrink:0;cursor:pointer",
704
+ title: "Copy URL",
705
+ children: copiedUrl ? "Copied!" : "Copy URL"
706
+ }
707
+ ),
708
+ /* @__PURE__ */ u3(
709
+ "button",
710
+ {
711
+ onClick: handleCopyCurl,
712
+ class: "profiler-body-download-btn profiler-text--xs",
713
+ style: "flex-shrink:0;cursor:pointer",
714
+ title: "Copy as cURL",
715
+ children: copiedCurl ? "Copied!" : "Copy as cURL"
716
+ }
717
+ )
718
+ ] }),
719
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", style: "flex-shrink:0", children: [
720
+ /* @__PURE__ */ u3("span", { class: `badge-${statusBadge(status)}`, children: status === 0 ? "ERR" : status }),
721
+ /* @__PURE__ */ u3("span", { class: duration >= 500 ? "badge-error" : duration >= 100 ? "badge-warning" : "badge-success", children: [
722
+ duration.toFixed(2),
723
+ " ms"
724
+ ] })
725
+ ] })
726
+ ]
727
+ }
728
+ );
729
+ }
730
+
731
+ // app/assets/typescript/profiler/components/dashboard/tabs/HttpTab.tsx
637
732
  function waterfallBarColor(status, duration) {
638
733
  if (status === 0 || status >= 500) return "var(--profiler-error, #ef4444)";
639
734
  if (status >= 400) return "var(--profiler-warning, #f59e0b)";
@@ -711,55 +806,21 @@
711
806
  }
712
807
  function HttpRequestDetail({ req, index, threshold }) {
713
808
  const [open, setOpen] = d2(false);
714
- const [copiedUrl, setCopiedUrl] = d2(false);
715
- const [copiedCurl, setCopiedCurl] = d2(false);
716
809
  const isError = req.status >= 400 || req.status === 0;
717
810
  const isSlow = req.duration >= threshold;
718
811
  const cardCls = isError ? "profiler-ajax-card--error" : isSlow ? "profiler-ajax-card--warning" : "profiler-ajax-card--success";
719
- function copyUrl(e3) {
720
- e3.stopPropagation();
721
- navigator.clipboard.writeText(req.url).then(() => {
722
- setCopiedUrl(true);
723
- setTimeout(() => setCopiedUrl(false), 2e3);
724
- });
725
- }
726
- function copyCurl() {
727
- navigator.clipboard.writeText(buildCurl(req)).then(() => {
728
- setCopiedCurl(true);
729
- setTimeout(() => setCopiedCurl(false), 2e3);
730
- });
731
- }
732
812
  return /* @__PURE__ */ u3("div", { class: `profiler-ajax-card ${cardCls}`, style: "margin-bottom:8px", children: [
733
813
  /* @__PURE__ */ u3(
734
- "div",
814
+ HttpCardHeader,
735
815
  {
736
- class: "profiler-ajax-card__row",
737
- style: "cursor:pointer;user-select:none",
738
- onClick: () => setOpen((o3) => !o3),
739
- children: [
740
- /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", style: "min-width:0;flex:1", children: [
741
- /* @__PURE__ */ u3("span", { style: "font-size:11px;color:var(--profiler-muted)", children: open ? "\u25BE" : "\u25B8" }),
742
- /* @__PURE__ */ u3("span", { class: `profiler-ajax-card__method badge-${methodBadge(req.method)}`, children: req.method }),
743
- /* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", style: "word-break:break-all", children: req.url }),
744
- /* @__PURE__ */ u3(
745
- "button",
746
- {
747
- onClick: copyUrl,
748
- class: "profiler-body-download-btn profiler-text--xs",
749
- style: "flex-shrink:0;cursor:pointer",
750
- title: "Copy URL",
751
- children: copiedUrl ? "Copied!" : "Copy URL"
752
- }
753
- )
754
- ] }),
755
- /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", style: "flex-shrink:0", children: [
756
- /* @__PURE__ */ u3("span", { class: `badge-${statusBadge(req.status)}`, children: req.status === 0 ? "ERR" : req.status }),
757
- /* @__PURE__ */ u3("span", { class: req.duration >= 500 ? "badge-error" : req.duration >= 100 ? "badge-warning" : "badge-success", children: [
758
- req.duration.toFixed(2),
759
- " ms"
760
- ] })
761
- ] })
762
- ]
816
+ method: req.method,
817
+ url: req.url,
818
+ status: req.status,
819
+ duration: req.duration,
820
+ curlCommand: buildCurl(req),
821
+ expandable: true,
822
+ open,
823
+ onToggle: () => setOpen((o3) => !o3)
763
824
  }
764
825
  ),
765
826
  /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
@@ -772,31 +833,14 @@
772
833
  req.backtrace && req.backtrace.length > 0 && /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", style: "margin-left:12px", children: req.backtrace[0] })
773
834
  ] }),
774
835
  req.error && /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--error", children: req.error }) }),
775
- open && /* @__PURE__ */ u3("div", { style: "padding:12px 4px 4px;border-top:1px solid rgba(0,0,0,0.08);margin-top:8px", children: [
776
- /* @__PURE__ */ u3("div", { style: "margin-bottom:12px", children: /* @__PURE__ */ u3("button", { onClick: copyCurl, class: "profiler-body-download-btn profiler-text--xs", style: "cursor:pointer", children: copiedCurl ? "Copied!" : "Copy as cURL" }) }),
777
- /* @__PURE__ */ u3("div", { style: "margin-bottom:16px", children: [
778
- /* @__PURE__ */ u3("div", { class: "profiler-text--sm", style: "font-weight:600;margin-bottom:8px;color:var(--profiler-muted)", children: "REQUEST" }),
779
- /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Headers" }),
780
- /* @__PURE__ */ u3(HeadersTable, { headers: req.request_headers || {} }),
781
- req.request_body && /* @__PURE__ */ u3(k, { children: [
782
- /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-top:10px;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Body" }),
783
- /* @__PURE__ */ u3(SmartBodyPreview, { body: req.request_body, encoding: req.request_body_encoding, headers: req.request_headers || {} })
784
- ] })
785
- ] }),
786
- /* @__PURE__ */ u3("div", { children: [
787
- /* @__PURE__ */ u3("div", { class: "profiler-text--sm", style: "font-weight:600;margin-bottom:8px;color:var(--profiler-muted)", children: "RESPONSE" }),
788
- /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Headers" }),
789
- /* @__PURE__ */ u3(HeadersTable, { headers: req.response_headers || {} }),
790
- req.response_body && /* @__PURE__ */ u3(k, { children: [
791
- /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-top:10px;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Body" }),
792
- /* @__PURE__ */ u3(SmartBodyPreview, { body: req.response_body, encoding: req.response_body_encoding, headers: req.response_headers || {} })
793
- ] })
794
- ] }),
795
- req.backtrace && req.backtrace.length > 1 && /* @__PURE__ */ u3("div", { style: "margin-top:12px", children: [
796
- /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Backtrace" }),
797
- req.backtrace.map((frame, i3) => /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "padding:2px 0", children: frame }, i3))
798
- ] })
799
- ] })
836
+ open && /* @__PURE__ */ u3(
837
+ HttpReqRespDetail,
838
+ {
839
+ request: { headers: req.request_headers || {}, body: req.request_body, body_encoding: req.request_body_encoding },
840
+ response: { headers: req.response_headers || {}, body: req.response_body, body_encoding: req.response_body_encoding },
841
+ backtrace: req.backtrace
842
+ }
843
+ )
800
844
  ] });
801
845
  }
802
846
  function HttpTab({ httpData }) {
@@ -946,9 +990,24 @@
946
990
  throw new Error(body.error ?? `Request failed (${res.status})`);
947
991
  }
948
992
  }
949
- function EnvTab({ envData }) {
993
+ async function resetEnvVar(key) {
994
+ const res = await fetch(`/_profiler/api/env_vars/reset?key=${encodeURIComponent(key)}`, {
995
+ method: "DELETE"
996
+ });
997
+ if (!res.ok) {
998
+ const body = await res.json().catch(() => ({}));
999
+ throw new Error(body.error ?? `Request failed (${res.status})`);
1000
+ }
1001
+ return res.json();
1002
+ }
1003
+ async function resetAllEnvVars() {
1004
+ const res = await fetch("/_profiler/api/env_vars/reset_all", { method: "DELETE" });
1005
+ if (!res.ok) throw new Error(`Request failed (${res.status})`);
1006
+ }
1007
+ function EnvTab({ envData, readOnly: forceReadOnly = false }) {
950
1008
  const initial = T2(() => ({ ...envData?.variables ?? {} }), []);
951
1009
  const [variables, setVariables] = d2(initial);
1010
+ const [overrides, setOverrides] = d2(envData?.overrides ?? {});
952
1011
  const [total, setTotal] = d2(envData?.total ?? 0);
953
1012
  const [search, setSearch] = d2("");
954
1013
  const [collapsedGroups, setCollapsedGroups] = d2(/* @__PURE__ */ new Set());
@@ -961,7 +1020,7 @@
961
1020
  const [refreshing, setRefreshing] = d2(false);
962
1021
  const [unmaskedKeys, setUnmaskedKeys] = d2(loadUnmasked);
963
1022
  const [copiedId, setCopiedId] = d2(null);
964
- const [readOnly, setReadOnly] = d2(false);
1023
+ const [readOnly, setReadOnly] = d2(forceReadOnly);
965
1024
  const [showImport, setShowImport] = d2(false);
966
1025
  const [importContent, setImportContent] = d2("");
967
1026
  const editInputRef = A2(null);
@@ -969,7 +1028,7 @@
969
1028
  if (editingKey !== null) editInputRef.current?.focus();
970
1029
  }, [editingKey]);
971
1030
  y2(() => {
972
- refresh();
1031
+ if (!forceReadOnly) refresh();
973
1032
  }, []);
974
1033
  const showFlash = (type, message) => {
975
1034
  setFlash({ type, message });
@@ -1009,6 +1068,7 @@
1009
1068
  const data = await res.json();
1010
1069
  setVariables(data.variables);
1011
1070
  setTotal(data.total);
1071
+ setOverrides(data.overrides ?? {});
1012
1072
  showFlash("success", `Refreshed \u2014 ${data.total} variables`);
1013
1073
  } catch (e3) {
1014
1074
  showFlash("error", e3.message ?? "Failed to refresh");
@@ -1122,6 +1182,45 @@
1122
1182
  if (e3.key === "Enter") saveEdit();
1123
1183
  if (e3.key === "Escape") cancelEdit();
1124
1184
  };
1185
+ const resetVar = async (key) => {
1186
+ setSaving(true);
1187
+ try {
1188
+ const data = await resetEnvVar(key);
1189
+ setOverrides((prev) => {
1190
+ const n2 = { ...prev };
1191
+ delete n2[key];
1192
+ return n2;
1193
+ });
1194
+ setVariables((prev) => {
1195
+ const n2 = { ...prev };
1196
+ if (data.value === null) {
1197
+ delete n2[key];
1198
+ } else {
1199
+ n2[key] = data.value;
1200
+ }
1201
+ return n2;
1202
+ });
1203
+ showFlash("success", `${key} reset to original`);
1204
+ } catch (e3) {
1205
+ showFlash("error", e3.message ?? "Failed to reset");
1206
+ } finally {
1207
+ setSaving(false);
1208
+ }
1209
+ };
1210
+ const doResetAll = async () => {
1211
+ setSaving(true);
1212
+ try {
1213
+ await resetAllEnvVars();
1214
+ setOverrides({});
1215
+ await refresh();
1216
+ showFlash("success", "All overrides reset");
1217
+ } catch (e3) {
1218
+ showFlash("error", e3.message ?? "Failed to reset all");
1219
+ } finally {
1220
+ setSaving(false);
1221
+ }
1222
+ };
1223
+ const overrideCount = Object.keys(overrides).length;
1125
1224
  const allEntries = Object.entries(variables);
1126
1225
  const filteredEntries = T2(() => {
1127
1226
  const q2 = search.toLowerCase().trim();
@@ -1161,11 +1260,27 @@
1161
1260
  /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-mb-4", style: "align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;", children: [
1162
1261
  /* @__PURE__ */ u3("h2", { class: "profiler-section__header", style: "margin:0;", children: "Environment Variables" }),
1163
1262
  /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", children: [
1164
- /* @__PURE__ */ u3("button", { onClick: refresh, disabled: refreshing, style: headerBtn, children: refreshing ? "\u2026" : "\u21BA Refresh" }),
1165
- /* @__PURE__ */ u3("button", { onClick: () => setShowImport((v3) => !v3), style: `${headerBtn}${showImport ? "border-color:var(--profiler-accent);color:var(--profiler-accent);" : ""}`, children: "\u2B06 Import" }),
1263
+ !forceReadOnly && /* @__PURE__ */ u3(k, { children: [
1264
+ /* @__PURE__ */ u3("button", { onClick: refresh, disabled: refreshing, style: headerBtn, children: refreshing ? "\u2026" : "\u21BA Refresh" }),
1265
+ /* @__PURE__ */ u3("button", { onClick: () => setShowImport((v3) => !v3), style: `${headerBtn}${showImport ? "border-color:var(--profiler-accent);color:var(--profiler-accent);" : ""}`, children: "\u2B06 Import" })
1266
+ ] }),
1166
1267
  /* @__PURE__ */ u3("button", { onClick: exportEnv, style: headerBtn, children: "\u2B07 Export" }),
1167
1268
  /* @__PURE__ */ u3("button", { onClick: toggleAllMasks, style: headerBtn, children: allRevealed ? "\u{1F648} Mask all" : "\u{1F441} Reveal all" }),
1168
- /* @__PURE__ */ u3(
1269
+ !forceReadOnly && overrideCount > 0 && /* @__PURE__ */ u3(
1270
+ "button",
1271
+ {
1272
+ onClick: doResetAll,
1273
+ disabled: saving,
1274
+ title: "Remove all Sidekiq overrides and restore original values",
1275
+ style: `${headerBtn}border-color:var(--profiler-warning,#f59e0b);color:var(--profiler-warning,#f59e0b);`,
1276
+ children: [
1277
+ "\u21A9 Reset all (",
1278
+ overrideCount,
1279
+ ")"
1280
+ ]
1281
+ }
1282
+ ),
1283
+ !forceReadOnly && /* @__PURE__ */ u3(
1169
1284
  "button",
1170
1285
  {
1171
1286
  onClick: () => setReadOnly((v3) => !v3),
@@ -1176,7 +1291,11 @@
1176
1291
  )
1177
1292
  ] })
1178
1293
  ] }),
1179
- /* @__PURE__ */ u3("div", { style: "background:var(--profiler-warning-bg,rgba(245,158,11,0.1));border:1px solid var(--profiler-warning,#f59e0b);border-radius:6px;padding:8px 12px;margin-bottom:16px;font-size:12px;color:var(--profiler-warning,#f59e0b);", children: "\u26A0 Changes affect the current process only \u2014 for development use." }),
1294
+ forceReadOnly ? /* @__PURE__ */ u3("div", { style: "background:var(--profiler-bg-subtle,rgba(0,0,0,0.03));border:1px solid var(--profiler-border);border-radius:6px;padding:8px 12px;margin-bottom:16px;font-size:12px;color:var(--profiler-text-muted);", children: [
1295
+ "Snapshot taken at request time \u2014 modify env vars in ",
1296
+ /* @__PURE__ */ u3("a", { href: "/_profiler?section=env", style: "color:var(--profiler-accent);", children: "/_profiler?section=env" }),
1297
+ "."
1298
+ ] }) : /* @__PURE__ */ u3("div", { style: "background:var(--profiler-warning-bg,rgba(245,158,11,0.1));border:1px solid var(--profiler-warning,#f59e0b);border-radius:6px;padding:8px 12px;margin-bottom:16px;font-size:12px;color:var(--profiler-warning,#f59e0b);", children: "\u26A0 Changes affect the current process only \u2014 for development use." }),
1180
1299
  flash && /* @__PURE__ */ u3("div", { style: `background:${flash.type === "success" ? "var(--profiler-success-bg,rgba(34,197,94,0.1))" : "var(--profiler-error-bg,rgba(239,68,68,0.08))"};border:1px solid ${flash.type === "success" ? "var(--profiler-success,#22c55e)" : "var(--profiler-error,#ef4444)"};border-radius:6px;padding:6px 12px;margin-bottom:12px;font-size:12px;color:${flash.type === "success" ? "var(--profiler-success,#22c55e)" : "var(--profiler-error,#ef4444)"};`, children: [
1181
1300
  flash.type === "success" ? "\u2713" : "\u2717",
1182
1301
  " ",
@@ -1271,7 +1390,8 @@
1271
1390
  const isCopiedKey = copiedId === `__key__${key}`;
1272
1391
  const wasModified = initial[key] !== void 0 && initial[key] !== value;
1273
1392
  const wasAdded = initial[key] === void 0;
1274
- const diffStyle = wasModified ? "border-left:3px solid #f59e0b;" : wasAdded ? "border-left:3px solid var(--profiler-success,#22c55e);" : "";
1393
+ const override = overrides[key];
1394
+ const diffStyle = override ? "border-left:3px solid var(--profiler-accent);" : wasModified ? "border-left:3px solid #f59e0b;" : wasAdded ? "border-left:3px solid var(--profiler-success,#22c55e);" : "";
1275
1395
  return /* @__PURE__ */ u3("tr", { style: diffStyle, children: [
1276
1396
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:4px;", children: [
1277
1397
  /* @__PURE__ */ u3("code", { class: "profiler-text--xs profiler-text--mono", children: key }),
@@ -1324,7 +1444,13 @@
1324
1444
  }
1325
1445
  )
1326
1446
  ] }),
1327
- wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
1447
+ override && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:var(--profiler-accent);margin-top:2px;font-family:monospace;", children: [
1448
+ "workers \u2190 ",
1449
+ override.value === "__profiler_deleted__" ? "(deleted)" : override.value,
1450
+ " \xB7 original: ",
1451
+ override.original ?? "(unset)"
1452
+ ] }),
1453
+ !override && wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
1328
1454
  "was: ",
1329
1455
  initial[key]
1330
1456
  ] })
@@ -1362,7 +1488,13 @@
1362
1488
  }
1363
1489
  )
1364
1490
  ] }),
1365
- wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
1491
+ override && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:var(--profiler-accent);margin-top:2px;font-family:monospace;", children: [
1492
+ "workers \u2190 ",
1493
+ override.value === "__profiler_deleted__" ? "(deleted)" : unmasked ? override.value : "\u2022".repeat(Math.min((override.value ?? "").length, 20)),
1494
+ " \xB7 original: ",
1495
+ override.original == null ? "(unset)" : unmasked ? override.original : "\u2022".repeat(Math.min(override.original.length, 20))
1496
+ ] }),
1497
+ !override && wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
1366
1498
  "was: ",
1367
1499
  unmasked ? isQuoted(initial[key]) ? `"${initial[key]}"` : initial[key] : "\u2022".repeat(Math.min(initial[key].length, 20))
1368
1500
  ] })
@@ -1370,11 +1502,18 @@
1370
1502
  !readOnly && /* @__PURE__ */ u3("td", { style: "text-align:right;white-space:nowrap;", children: isEditing ? /* @__PURE__ */ u3(k, { children: [
1371
1503
  /* @__PURE__ */ u3("button", { onClick: saveEdit, disabled: saving, title: "Save", style: "background:none;border:none;cursor:pointer;color:var(--profiler-success,#22c55e);font-size:14px;padding:0 4px;", children: "\u2713" }),
1372
1504
  /* @__PURE__ */ u3("button", { onClick: cancelEdit, disabled: saving, title: "Cancel", style: "background:none;border:none;cursor:pointer;color:var(--profiler-text-muted);font-size:14px;padding:0 4px;", children: "\u2717" })
1373
- ] }) : isBool(value) ? /* @__PURE__ */ u3(k, { children: [
1374
- /* @__PURE__ */ u3("button", { onClick: () => startEdit(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-accent);font-size:11px;padding:0 4px;", children: "Edit" }),
1375
- /* @__PURE__ */ u3("button", { onClick: () => deleteVar(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-error,#ef4444);font-size:11px;padding:0 4px;", children: "Delete" })
1376
1505
  ] }) : /* @__PURE__ */ u3(k, { children: [
1377
1506
  /* @__PURE__ */ u3("button", { onClick: () => startEdit(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-accent);font-size:11px;padding:0 4px;", children: "Edit" }),
1507
+ override && /* @__PURE__ */ u3(
1508
+ "button",
1509
+ {
1510
+ onClick: () => resetVar(key),
1511
+ disabled: saving,
1512
+ title: `Reset to original: ${override.original ?? "(unset)"}`,
1513
+ style: "background:none;border:none;cursor:pointer;color:var(--profiler-warning,#f59e0b);font-size:11px;padding:0 4px;",
1514
+ children: "\u21A9 Reset"
1515
+ }
1516
+ ),
1378
1517
  /* @__PURE__ */ u3("button", { onClick: () => deleteVar(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-error,#ef4444);font-size:11px;padding:0 4px;", children: "Delete" })
1379
1518
  ] }) })
1380
1519
  ] }, key);
@@ -2100,107 +2239,43 @@
2100
2239
  return parts.join(" \\\n");
2101
2240
  }
2102
2241
  function RequestTab({ profile }) {
2103
- const [copied, setCopied] = d2(false);
2104
- const curl = buildCurl2(profile);
2105
2242
  const routeData = profile.collectors_data?.request ?? {};
2106
- function copyToClipboard() {
2107
- navigator.clipboard.writeText(curl).then(() => {
2108
- setCopied(true);
2109
- setTimeout(() => setCopied(false), 2e3);
2110
- });
2111
- }
2112
- return /* @__PURE__ */ u3(k, { children: [
2113
- /* @__PURE__ */ u3("h2", { class: "profiler-section__header", children: "Request" }),
2114
- /* @__PURE__ */ u3("table", { children: [
2115
- /* @__PURE__ */ u3("tr", { children: [
2116
- /* @__PURE__ */ u3("th", { class: "profiler-text--sm", style: "width: 200px;", children: "Path" }),
2117
- /* @__PURE__ */ u3("td", { children: profile.path })
2118
- ] }),
2119
- /* @__PURE__ */ u3("tr", { children: [
2120
- /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Method" }),
2121
- /* @__PURE__ */ u3("td", { children: profile.method })
2122
- ] }),
2123
- /* @__PURE__ */ u3("tr", { children: [
2124
- /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Status" }),
2125
- /* @__PURE__ */ u3("td", { children: profile.status })
2126
- ] }),
2127
- /* @__PURE__ */ u3("tr", { children: [
2128
- /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Duration" }),
2129
- /* @__PURE__ */ u3("td", { children: [
2130
- profile.duration.toFixed(2),
2131
- " ms"
2132
- ] })
2133
- ] }),
2134
- routeData.controller_action && /* @__PURE__ */ u3("tr", { children: [
2135
- /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Controller#Action" }),
2136
- /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: routeData.controller_action })
2137
- ] }),
2138
- routeData.route_name && /* @__PURE__ */ u3("tr", { children: [
2139
- /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Route Name" }),
2140
- /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: routeData.route_name })
2141
- ] }),
2142
- routeData.route_pattern && /* @__PURE__ */ u3("tr", { children: [
2143
- /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Route Pattern" }),
2144
- /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: routeData.route_pattern })
2145
- ] }),
2146
- routeData.route_params && Object.keys(routeData.route_params).length > 0 && /* @__PURE__ */ u3("tr", { children: [
2147
- /* @__PURE__ */ u3("th", { class: "profiler-text--sm", children: "Route Params" }),
2148
- /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: Object.entries(routeData.route_params).map(([k3, v3]) => `${k3}: ${v3}`).join(", ") })
2243
+ const hasParams = profile.params && Object.keys(profile.params).length > 0;
2244
+ return /* @__PURE__ */ u3("div", { class: "profiler-ajax-card profiler-ajax-card--success", style: "margin-bottom:8px;background:var(--profiler-bg-elevated);transform:translateX(3px);box-shadow:var(--profiler-shadow-sm);transition:none", children: [
2245
+ /* @__PURE__ */ u3(
2246
+ HttpCardHeader,
2247
+ {
2248
+ method: profile.method,
2249
+ url: profile.path,
2250
+ copyUrl: `http://localhost:3000${profile.path}`,
2251
+ status: profile.status,
2252
+ duration: profile.duration,
2253
+ curlCommand: buildCurl2(profile)
2254
+ }
2255
+ ),
2256
+ (routeData.controller_action || routeData.route_pattern) && /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
2257
+ routeData.controller_action && /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", style: "margin-right:16px", children: routeData.controller_action }),
2258
+ routeData.route_pattern && /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", style: "font-family:monospace", children: [
2259
+ routeData.route_name ? `${routeData.route_name} \xB7 ` : "",
2260
+ routeData.route_pattern
2149
2261
  ] })
2150
2262
  ] }),
2151
- profile.headers && Object.keys(profile.headers).length > 0 && /* @__PURE__ */ u3(k, { children: [
2152
- /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-6", children: "Request Headers" }),
2153
- /* @__PURE__ */ u3("table", { children: Object.entries(profile.headers).map(([k3, v3]) => /* @__PURE__ */ u3("tr", { children: [
2154
- /* @__PURE__ */ u3("th", { class: "profiler-text--sm", style: "width: 200px;", children: k3 }),
2155
- /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: String(v3) })
2156
- ] }, k3)) })
2157
- ] }),
2158
- profile.params && Object.keys(profile.params).length > 0 && /* @__PURE__ */ u3(k, { children: [
2159
- /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-6", children: "Request Params" }),
2160
- /* @__PURE__ */ u3("pre", { class: "profiler-code profiler-text--xs", children: JSON.stringify(profile.params, null, 2) })
2161
- ] }),
2162
- profile.request_body && /* @__PURE__ */ u3(k, { children: [
2163
- /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-6", children: "Request Body" }),
2164
- /* @__PURE__ */ u3(
2165
- SmartBodyPreview,
2166
- {
2263
+ /* @__PURE__ */ u3(
2264
+ HttpReqRespDetail,
2265
+ {
2266
+ request: {
2267
+ headers: profile.headers ?? {},
2167
2268
  body: profile.request_body,
2168
- encoding: profile.request_body_encoding,
2169
- headers: profile.headers ?? {}
2170
- }
2171
- )
2172
- ] }),
2173
- profile.response_headers && Object.keys(profile.response_headers).length > 0 && /* @__PURE__ */ u3(k, { children: [
2174
- /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-6", children: "Response Headers" }),
2175
- /* @__PURE__ */ u3("table", { children: Object.entries(profile.response_headers).map(([k3, v3]) => /* @__PURE__ */ u3("tr", { children: [
2176
- /* @__PURE__ */ u3("th", { class: "profiler-text--sm", style: "width: 200px;", children: k3 }),
2177
- /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: String(v3) })
2178
- ] }, k3)) })
2179
- ] }),
2180
- profile.response_body && /* @__PURE__ */ u3(k, { children: [
2181
- /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-6", children: "Response Body" }),
2182
- /* @__PURE__ */ u3(
2183
- SmartBodyPreview,
2184
- {
2269
+ body_encoding: profile.request_body_encoding,
2270
+ params: hasParams ? profile.params : void 0
2271
+ },
2272
+ response: {
2273
+ headers: profile.response_headers ?? {},
2185
2274
  body: profile.response_body,
2186
- encoding: profile.response_body_encoding,
2187
- headers: profile.response_headers ?? {}
2275
+ body_encoding: profile.response_body_encoding
2188
2276
  }
2189
- )
2190
- ] }),
2191
- /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-6", children: "Curl Command" }),
2192
- /* @__PURE__ */ u3("div", { style: "position: relative;", children: [
2193
- /* @__PURE__ */ u3(
2194
- "button",
2195
- {
2196
- onClick: copyToClipboard,
2197
- class: "profiler-body-download-btn",
2198
- style: "position: absolute; top: 8px; right: 8px; z-index: 1;",
2199
- children: copied ? "Copied!" : "Copy"
2200
- }
2201
- ),
2202
- /* @__PURE__ */ u3("pre", { class: "profiler-code profiler-text--xs", style: "padding-right: 80px;", children: curl })
2203
- ] })
2277
+ }
2278
+ )
2204
2279
  ] });
2205
2280
  }
2206
2281
 
@@ -3081,12 +3156,6 @@
3081
3156
  custom: "Custom",
3082
3157
  method: "Method"
3083
3158
  };
3084
- function formatBytes3(bytes) {
3085
- if (bytes < 0) return "\u2014";
3086
- if (bytes < 1024) return `${bytes} B`;
3087
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3088
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
3089
- }
3090
3159
  function FlameGraphTab({ flamegraphData, perfData, functionProfileData }) {
3091
3160
  const canvasRef = A2(null);
3092
3161
  const containerRef = A2(null);
@@ -3592,7 +3661,7 @@
3592
3661
  ] }),
3593
3662
  showMemory && /* @__PURE__ */ u3("div", { class: "stat-item", children: [
3594
3663
  /* @__PURE__ */ u3("span", { class: "stat-label", children: "Memory" }),
3595
- /* @__PURE__ */ u3("span", { class: "stat-value", children: formatBytes3(data.total_memory_bytes ?? 0) })
3664
+ /* @__PURE__ */ u3("span", { class: "stat-value", children: formatBytes(data.total_memory_bytes ?? 0) })
3596
3665
  ] })
3597
3666
  ] }),
3598
3667
  rootCalls.length > 0 && /* @__PURE__ */ u3(
@@ -3698,7 +3767,7 @@
3698
3767
  " ",
3699
3768
  /* @__PURE__ */ u3("small", { children: "obj" })
3700
3769
  ] }),
3701
- showMemory && /* @__PURE__ */ u3("td", { class: "profiler-text--right profiler-text--muted", children: formatBytes3(fn.memory_bytes) })
3770
+ showMemory && /* @__PURE__ */ u3("td", { class: "profiler-text--right profiler-text--muted", children: formatBytes(fn.memory_bytes) })
3702
3771
  ]
3703
3772
  },
3704
3773
  i3
@@ -3835,15 +3904,6 @@
3835
3904
  }
3836
3905
 
3837
3906
  // app/assets/typescript/profiler/components/dashboard/tabs/AjaxTab.tsx
3838
- function methodBadge2(method) {
3839
- const map = { GET: "info", POST: "success", PUT: "warning", PATCH: "warning", DELETE: "error" };
3840
- return map[method] || "default";
3841
- }
3842
- function statusBadge2(status) {
3843
- if (status >= 200 && status < 300) return "success";
3844
- if (status >= 400) return "error";
3845
- return "warning";
3846
- }
3847
3907
  function AjaxTab({ ajaxData }) {
3848
3908
  if (!ajaxData?.requests?.length) {
3849
3909
  return /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
@@ -3879,7 +3939,7 @@
3879
3939
  /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-panel--sm", children: [
3880
3940
  /* @__PURE__ */ u3("h3", { class: "profiler-text--sm profiler-text--muted profiler-text--uppercase profiler-mb-3", children: "By Method" }),
3881
3941
  Object.entries(ajaxData.by_method).map(([method, count]) => /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
3882
- /* @__PURE__ */ u3("span", { class: `badge-${methodBadge2(method)}`, children: method }),
3942
+ /* @__PURE__ */ u3("span", { class: `badge-${methodBadge(method)}`, children: method }),
3883
3943
  /* @__PURE__ */ u3("strong", { children: count })
3884
3944
  ] }, method))
3885
3945
  ] }),
@@ -3895,11 +3955,11 @@
3895
3955
  ajaxData.requests.map((req, index) => /* @__PURE__ */ u3("div", { class: `profiler-ajax-card profiler-ajax-card--${req.status >= 200 && req.status < 300 ? "success" : "error"}`, children: [
3896
3956
  /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
3897
3957
  /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", children: [
3898
- /* @__PURE__ */ u3("span", { class: `profiler-ajax-card__method badge-${methodBadge2(req.method)}`, children: req.method }),
3958
+ /* @__PURE__ */ u3("span", { class: `profiler-ajax-card__method badge-${methodBadge(req.method)}`, children: req.method }),
3899
3959
  /* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", children: req.path })
3900
3960
  ] }),
3901
3961
  /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", children: [
3902
- /* @__PURE__ */ u3("span", { class: `badge-${statusBadge2(req.status)}`, children: req.status }),
3962
+ /* @__PURE__ */ u3("span", { class: `badge-${statusBadge(req.status)}`, children: req.status }),
3903
3963
  /* @__PURE__ */ u3("span", { class: req.duration >= 500 ? "badge-error" : req.duration >= 100 ? "badge-warning" : "badge-success", children: [
3904
3964
  req.duration.toFixed(2),
3905
3965
  " ms"
@@ -4420,7 +4480,7 @@
4420
4480
  activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
4421
4481
  activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] }),
4422
4482
  activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
4423
- activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"] })
4483
+ activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"], readOnly: true })
4424
4484
  ] })
4425
4485
  ] }),
4426
4486
  !embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
@@ -4483,12 +4543,15 @@
4483
4543
 
4484
4544
  // app/assets/typescript/profiler/components/dashboard/JobProfileDashboard.tsx
4485
4545
  function JobProfileDashboard({ profile, initialTab, embedded }) {
4486
- const validTabs = ["job", "database", "cache", "http", "jobs"];
4487
- const defaultTab = validTabs.includes(initialTab) ? initialTab : "job";
4488
- const [activeTab, setActiveTab] = d2(defaultTab);
4489
4546
  const cd = profile.collectors_data || {};
4490
4547
  const hasHttp = cd["http"]?.total_requests > 0;
4491
4548
  const hasJobs = (profile.child_jobs?.length ?? 0) > 0;
4549
+ const hasDumps = (cd["dump"]?.count ?? 0) > 0;
4550
+ const hasLogs = (cd["logs"]?.total ?? 0) > 0;
4551
+ const hasException = !!cd["exception"]?.exception_class;
4552
+ const validTabs = ["job", "database", "cache", "http", "jobs", "dump", "logs", "exception", "env", "timeline"];
4553
+ const defaultTab = validTabs.includes(initialTab) ? initialTab : "job";
4554
+ const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
4492
4555
  const jobData = cd["job"];
4493
4556
  const isFailed = jobData?.status === "failed";
4494
4557
  const parent = profile.parent_profile;
@@ -4550,22 +4613,36 @@
4550
4613
  ] }),
4551
4614
  /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
4552
4615
  /* @__PURE__ */ u3("div", { class: "tabs", children: [
4616
+ hasException && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("exception"), onClick: handleTabClick("exception"), style: "color:var(--profiler-error,#ef4444);", children: "\u{1F4A5} Exception" }),
4553
4617
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("job"), onClick: handleTabClick("job"), children: "Job" }),
4554
4618
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("database"), onClick: handleTabClick("database"), children: "Database" }),
4555
4619
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
4556
4620
  hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "Outbound HTTP" }),
4621
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("timeline"), onClick: handleTabClick("timeline"), children: "Timeline" }),
4557
4622
  hasJobs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("jobs"), onClick: handleTabClick("jobs"), children: [
4558
4623
  "Jobs (",
4559
4624
  profile.child_jobs.length,
4560
4625
  ")"
4561
- ] })
4626
+ ] }),
4627
+ hasDumps && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("dump"), onClick: handleTabClick("dump"), children: [
4628
+ "Dumps (",
4629
+ cd["dump"].count,
4630
+ ")"
4631
+ ] }),
4632
+ hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
4633
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" })
4562
4634
  ] }),
4563
4635
  /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
4636
+ activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
4564
4637
  activeTab === "job" && /* @__PURE__ */ u3(JobTab, { jobData: cd["job"] }),
4565
4638
  activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
4566
4639
  activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
4567
4640
  activeTab === "http" && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] }),
4568
- activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs })
4641
+ activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, { flamegraphData: cd["flamegraph"], perfData: cd["performance"], functionProfileData: cd["function_profile"] }),
4642
+ activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
4643
+ activeTab === "dump" && /* @__PURE__ */ u3(DumpsTab, { dumpData: cd["dump"] }),
4644
+ activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
4645
+ activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"], readOnly: true })
4569
4646
  ] })
4570
4647
  ] }),
4571
4648
  !embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
@@ -7,7 +7,8 @@ module Profiler
7
7
 
8
8
  def show
9
9
  variables = ENV.to_h.sort.to_h
10
- render json: { variables: variables, total: variables.size }
10
+ overrides = Profiler.env_override_store.all_overrides
11
+ render json: { variables: variables, total: variables.size, overrides: overrides }
11
12
  end
12
13
 
13
14
  def update
@@ -22,12 +23,31 @@ module Profiler
22
23
 
23
24
  if value.nil? || value.to_s.empty?
24
25
  ENV.delete(key)
26
+ Profiler.env_override_store.delete(key)
25
27
  render json: { key: key, value: nil, deleted: true }
26
28
  else
27
29
  ENV[key] = value.to_s
30
+ Profiler.env_override_store.set(key, value.to_s)
28
31
  render json: { key: key, value: ENV[key] }
29
32
  end
30
33
  end
34
+
35
+ def reset_override
36
+ key = params[:key].to_s.strip
37
+
38
+ if key.blank?
39
+ render json: { error: "Key cannot be blank" }, status: :unprocessable_entity
40
+ return
41
+ end
42
+
43
+ Profiler.env_override_store.reset(key)
44
+ render json: { key: key, value: ENV[key], reset: true }
45
+ end
46
+
47
+ def reset_all
48
+ Profiler.env_override_store.reset_all
49
+ render json: { reset: true }
50
+ end
31
51
  end
32
52
  end
33
53
  end
data/config/routes.rb CHANGED
@@ -35,5 +35,7 @@ Profiler::Engine.routes.draw do
35
35
  post "explain", to: "explain#create"
36
36
  resource :function_profiling, only: [:show, :update], controller: "function_profiling"
37
37
  resource :env_vars, only: [:show, :update], controller: "env_vars"
38
+ delete "env_vars/reset", to: "env_vars#reset_override"
39
+ delete "env_vars/reset_all", to: "env_vars#reset_all"
38
40
  end
39
41
  end
@@ -38,6 +38,10 @@ module Profiler
38
38
  end
39
39
  end
40
40
 
41
+ def capture(ex)
42
+ capture_exception(ex) if ex && @exception_data.nil?
43
+ end
44
+
41
45
  def collect
42
46
  ActiveSupport::Notifications.unsubscribe(@subscriber) if @subscriber
43
47
 
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module Profiler
7
+ class EnvOverrideStore
8
+ DELETED_SENTINEL = "__profiler_deleted__"
9
+ RESTORE_SENTINEL = "__profiler_restore__"
10
+
11
+ # File format: { "KEY" => { "value" => "...", "original" => "..." } }
12
+ # Sentinels for "value":
13
+ # DELETED_SENTINEL → ENV.delete(key)
14
+ # RESTORE_SENTINEL → restore ENV[key] to "original" (cleaned up after apply!)
15
+
16
+ def set(key, value)
17
+ overrides = load_overrides
18
+ original = original_for(overrides, key)
19
+ overrides[key] = { "value" => value.to_s, "original" => original }
20
+ save_overrides(overrides)
21
+ rescue => e
22
+ warn "[Profiler] EnvOverrideStore: failed to set #{key}: #{e.message}"
23
+ end
24
+
25
+ def delete(key)
26
+ overrides = load_overrides
27
+ original = original_for(overrides, key)
28
+ overrides[key] = { "value" => DELETED_SENTINEL, "original" => original }
29
+ save_overrides(overrides)
30
+ rescue => e
31
+ warn "[Profiler] EnvOverrideStore: failed to delete #{key}: #{e.message}"
32
+ end
33
+
34
+ def reset(key)
35
+ overrides = load_overrides
36
+ entry = overrides[key]
37
+ return unless entry
38
+
39
+ original = entry["original"]
40
+ # Keep a RESTORE entry so Sidekiq workers pick it up on next job
41
+ overrides[key] = { "value" => RESTORE_SENTINEL, "original" => original }
42
+ save_overrides(overrides)
43
+
44
+ # Apply immediately to the current (web) process
45
+ original.nil? ? ENV.delete(key) : ENV[key] = original
46
+ rescue => e
47
+ warn "[Profiler] EnvOverrideStore: failed to reset #{key}: #{e.message}"
48
+ end
49
+
50
+ def reset_all
51
+ overrides = load_overrides
52
+ restore_overrides = {}
53
+
54
+ overrides.each do |key, entry|
55
+ original = entry.is_a?(Hash) ? entry["original"] : nil
56
+ # Apply immediately to the current (web) process
57
+ original.nil? ? ENV.delete(key) : ENV[key] = original
58
+ # Leave a RESTORE sentinel for Sidekiq workers to pick up
59
+ restore_overrides[key] = { "value" => RESTORE_SENTINEL, "original" => original }
60
+ end
61
+
62
+ restore_overrides.empty? ? clear : save_overrides(restore_overrides)
63
+ rescue => e
64
+ warn "[Profiler] EnvOverrideStore: failed to reset all: #{e.message}"
65
+ end
66
+
67
+ def apply!
68
+ overrides = load_overrides
69
+ restore_keys = []
70
+
71
+ overrides.each do |key, entry|
72
+ value = entry.is_a?(Hash) ? entry["value"] : entry
73
+ original = entry.is_a?(Hash) ? entry["original"] : nil
74
+
75
+ case value
76
+ when DELETED_SENTINEL
77
+ ENV.delete(key)
78
+ when RESTORE_SENTINEL
79
+ original.nil? ? ENV.delete(key) : ENV[key] = original
80
+ restore_keys << key
81
+ else
82
+ ENV[key] = value
83
+ end
84
+ end
85
+
86
+ if restore_keys.any?
87
+ restore_keys.each { |k| overrides.delete(k) }
88
+ save_overrides(overrides)
89
+ end
90
+ rescue => e
91
+ warn "[Profiler] EnvOverrideStore: failed to apply overrides: #{e.message}"
92
+ end
93
+
94
+ # Returns active overrides (excludes RESTORE entries — already being reverted)
95
+ def all_overrides
96
+ load_overrides.reject do |_, entry|
97
+ value = entry.is_a?(Hash) ? entry["value"] : entry
98
+ value == RESTORE_SENTINEL
99
+ end.transform_values do |entry|
100
+ entry.is_a?(Hash) ? entry : { "value" => entry, "original" => nil }
101
+ end
102
+ rescue => e
103
+ warn "[Profiler] EnvOverrideStore: failed to load overrides: #{e.message}"
104
+ {}
105
+ end
106
+
107
+ def clear
108
+ FileUtils.rm_f(override_file_path)
109
+ rescue => e
110
+ warn "[Profiler] EnvOverrideStore: failed to clear: #{e.message}"
111
+ end
112
+
113
+ private
114
+
115
+ def original_for(overrides, key)
116
+ overrides.key?(key) ? overrides[key]["original"] : ENV[key]
117
+ end
118
+
119
+ def override_file_path
120
+ Profiler.configuration.tmp_path.join("env_overrides.json")
121
+ end
122
+
123
+ def load_overrides
124
+ path = override_file_path
125
+ return {} unless File.exist?(path)
126
+
127
+ JSON.parse(File.read(path))
128
+ rescue JSON::ParserError
129
+ {}
130
+ end
131
+
132
+ def save_overrides(overrides)
133
+ path = override_file_path
134
+ FileUtils.mkdir_p(path.dirname)
135
+ File.write(path, JSON.generate(overrides))
136
+ end
137
+ end
138
+ end
@@ -22,6 +22,9 @@ module Profiler
22
22
 
23
23
  url = build_url(host, port, req.path, use_ssl?)
24
24
  req_body = req.body.to_s
25
+ # Fallback: body may be passed as the 2nd argument and only applied
26
+ # to req inside super via req.set_body_internal(body)
27
+ req_body = body.to_s if req_body.empty? && body
25
28
  req_headers = req.to_hash.transform_values { |v| v.join(", ") }
26
29
  request_id = SecureRandom.hex(8)
27
30
  started_at = Time.now.iso8601(3)
@@ -11,6 +11,7 @@ module Profiler
11
11
 
12
12
  class SidekiqMiddleware
13
13
  def call(worker, job, queue, &block)
14
+ Profiler.env_override_store.apply!
14
15
  Profiler::JobProfiler.profile(
15
16
  job_class: job["class"],
16
17
  job_id: job["jid"],
@@ -6,13 +6,23 @@ require_relative "collectors/job_collector"
6
6
  require_relative "collectors/database_collector"
7
7
  require_relative "collectors/cache_collector"
8
8
  require_relative "collectors/http_collector"
9
+ require_relative "collectors/dump_collector"
10
+ require_relative "collectors/log_collector"
11
+ require_relative "collectors/exception_collector"
12
+ require_relative "collectors/env_collector"
13
+ require_relative "collectors/flamegraph_collector"
9
14
 
10
15
  module Profiler
11
16
  class JobProfiler
12
17
  JOB_COLLECTOR_CLASSES = [
13
18
  Collectors::DatabaseCollector,
14
19
  Collectors::CacheCollector,
15
- Collectors::HttpCollector
20
+ Collectors::HttpCollector,
21
+ Collectors::DumpCollector,
22
+ Collectors::LogCollector,
23
+ Collectors::ExceptionCollector,
24
+ Collectors::EnvCollector,
25
+ Collectors::FlameGraphCollector
16
26
  ].freeze
17
27
 
18
28
  def self.profile(job_class:, job_id:, queue:, arguments:, executions:, parent_token: nil, &block)
@@ -55,6 +65,8 @@ module Profiler
55
65
  collectors = [job_collector] + JOB_COLLECTOR_CLASSES.map { |klass| klass.new(profile) }
56
66
  collectors.each { |c| c.subscribe if c.respond_to?(:subscribe) }
57
67
 
68
+ exception_collector = collectors.find { |c| c.is_a?(Collectors::ExceptionCollector) }
69
+
58
70
  memory_before = current_memory if Profiler.configuration.track_memory
59
71
 
60
72
  job_status = "completed"
@@ -68,6 +80,7 @@ module Profiler
68
80
  rescue => e
69
81
  job_status = "failed"
70
82
  error_message = "#{e.class}: #{e.message}"
83
+ exception_collector&.capture(e)
71
84
  raise
72
85
  ensure
73
86
  Profiler::CurrentContext.token = previous_token
@@ -6,6 +6,10 @@ module Profiler
6
6
  class Railtie < Rails::Railtie
7
7
  config.profiler = ActiveSupport::OrderedOptions.new
8
8
 
9
+ initializer "profiler.apply_env_overrides" do
10
+ Profiler.env_override_store.apply!
11
+ end
12
+
9
13
  initializer "profiler.set_configs" do |app|
10
14
  # Set default configuration for Rails environment
11
15
  Profiler.configure do |config|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.17.0"
4
+ VERSION = "0.19.0"
5
5
  end
data/lib/profiler.rb CHANGED
@@ -25,6 +25,10 @@ module Profiler
25
25
  @storage ||= configuration.storage_backend
26
26
  end
27
27
 
28
+ def env_override_store
29
+ @env_override_store ||= EnvOverrideStore.new
30
+ end
31
+
28
32
  def enabled?
29
33
  configuration.enabled
30
34
  end
@@ -100,5 +104,6 @@ require_relative "profiler/collectors/routes_collector"
100
104
  require_relative "profiler/collectors/i18n_collector"
101
105
  require_relative "profiler/collectors/env_collector"
102
106
 
107
+ require_relative "profiler/env_override_store"
103
108
  require_relative "profiler/railtie" if defined?(Rails::Railtie)
104
109
  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.17.0
4
+ version: 0.19.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-13 00:00:00.000000000 Z
11
+ date: 2026-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -143,6 +143,7 @@ files:
143
143
  - lib/profiler/configuration.rb
144
144
  - lib/profiler/current_context.rb
145
145
  - lib/profiler/engine.rb
146
+ - lib/profiler/env_override_store.rb
146
147
  - lib/profiler/explain_runner.rb
147
148
  - lib/profiler/instrumentation/active_job_instrumentation.rb
148
149
  - lib/profiler/instrumentation/net_http_instrumentation.rb