rails-profiler 0.21.0 → 0.22.1

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: 80a34504e3cb67a077d5f46265cbc265cbf7704f89ad06f90e5ab5df22651a70
4
- data.tar.gz: d23a6d0d9f107611d66dfe34987447efde5f74f2562efd146d43d910fe431c6a
3
+ metadata.gz: 87931b523c1fcf03710fe558f619591bc2a953aa50ad76ccf414357964515a5d
4
+ data.tar.gz: c07a2ae3e5104529f72d4ce04caf67aa04647946c4ba5e98fc68ca1c7b10c320
5
5
  SHA512:
6
- metadata.gz: 668d228ede43471fc71a5d65d6498bfd00606865f7cad69dd23e03f79a4e84a2ba5f34dbc2d9e07809df4319946055464b7b8e615ee29e14c8cfafdef10ffc4a
7
- data.tar.gz: 16926d1a141c64cc72110698cd21e4d5c26d6e9022cd17ad6ba5ba3112aa98ce1917db9d005c112fac62190790433c06b24f1d9a85648edabf679664eb89b30d
6
+ metadata.gz: ffb96a532e285ff59fe55059a2a0615c397ff82838479da8804709e14e7ec1802a0b759ffdd32ed64bb9166ea185efd8da06956cab3078c43bf4e6120b8d7c56
7
+ data.tar.gz: 22de884813f0d456a53c2388bb3849ff343bbd4349951419ab2f04f3c1977ce0e1681ba5c8c5349fff3cb894d6f2a8b34de9cf13f92acce08921ec3298fe816c
@@ -1010,6 +1010,60 @@
1010
1010
  ] });
1011
1011
  }
1012
1012
 
1013
+ // app/assets/typescript/profiler/components/toolbar/panels/MailerPanel.tsx
1014
+ function MailerPanel({ mailerData }) {
1015
+ const hasErrors = mailerData.failed > 0 || mailerData.loop_warnings.length > 0;
1016
+ return /* @__PURE__ */ u3(k, { children: [
1017
+ /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-header", children: [
1018
+ "Mailers",
1019
+ /* @__PURE__ */ u3("span", { class: "profiler-float-right", children: [
1020
+ mailerData.total,
1021
+ " email",
1022
+ mailerData.total !== 1 ? "s" : ""
1023
+ ] })
1024
+ ] }),
1025
+ /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-content", children: [
1026
+ mailerData.deliver_now > 0 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
1027
+ /* @__PURE__ */ u3("span", { children: "deliver_now" }),
1028
+ /* @__PURE__ */ u3("strong", { children: mailerData.deliver_now })
1029
+ ] }),
1030
+ mailerData.deliver_later > 0 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
1031
+ /* @__PURE__ */ u3("span", { children: "deliver_later" }),
1032
+ /* @__PURE__ */ u3("strong", { children: mailerData.deliver_later })
1033
+ ] }),
1034
+ (mailerData.queued_count ?? 0) > 0 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
1035
+ /* @__PURE__ */ u3("span", { children: "Queued" }),
1036
+ /* @__PURE__ */ u3("strong", { children: mailerData.queued_count })
1037
+ ] }),
1038
+ mailerData.multi_part_count > 0 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
1039
+ /* @__PURE__ */ u3("span", { children: "Multi-part" }),
1040
+ /* @__PURE__ */ u3("strong", { children: mailerData.multi_part_count })
1041
+ ] }),
1042
+ mailerData.failed > 0 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
1043
+ /* @__PURE__ */ u3("span", { children: "Errors" }),
1044
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--error", children: mailerData.failed })
1045
+ ] }),
1046
+ mailerData.loop_warnings.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
1047
+ /* @__PURE__ */ u3("span", { class: "profiler-text--warning", children: "\u26A0\uFE0F Loop detected" }),
1048
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--warning", children: mailerData.loop_warnings.length })
1049
+ ] }),
1050
+ [...mailerData.emails, ...mailerData.errors].slice(0, 3).map((email, i3) => /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row profiler-text--sm", children: [
1051
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: [
1052
+ email.mailer_class,
1053
+ "#",
1054
+ email.action
1055
+ ] }),
1056
+ /* @__PURE__ */ u3("span", { class: email.error ? "profiler-text--error" : "profiler-text--success", children: email.error ? "\u274C" : "\u2705" })
1057
+ ] }, i3)),
1058
+ mailerData.total > 3 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row profiler-text--muted profiler-text--xs", children: [
1059
+ "+",
1060
+ mailerData.total - 3,
1061
+ " more\u2026"
1062
+ ] })
1063
+ ] })
1064
+ ] });
1065
+ }
1066
+
1013
1067
  // app/assets/typescript/profiler/components/toolbar/ToolbarApp.tsx
1014
1068
  function statusClass2(status) {
1015
1069
  if (status >= 200 && status < 300) return "profiler-text--success";
@@ -1045,6 +1099,7 @@
1045
1099
  const routesData = cd["routes"];
1046
1100
  const i18nData = cd["i18n"];
1047
1101
  const envData = cd["env"];
1102
+ const mailerData = cd["mailer"];
1048
1103
  const childJobs = profile.child_jobs ?? [];
1049
1104
  const reqClass = statusClass2(profile.status);
1050
1105
  const durClass = durationClass(profile.duration);
@@ -1283,6 +1338,32 @@
1283
1338
  ]
1284
1339
  }
1285
1340
  ),
1341
+ mailerData && (mailerData.total > 0 || (mailerData.queued_count ?? 0) > 0) && /* @__PURE__ */ u3(
1342
+ ToolbarItem,
1343
+ {
1344
+ href: `/_profiler/profiles/${token}?tab=mailer`,
1345
+ className: mailerData.failed > 0 ? "profiler-text--error" : "profiler-text--success",
1346
+ panelLarge: true,
1347
+ panel: /* @__PURE__ */ u3(MailerPanel, { mailerData }),
1348
+ children: [
1349
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "MAIL" }),
1350
+ mailerData.total > 0 && /* @__PURE__ */ u3("span", { children: mailerData.total }),
1351
+ mailerData.total === 0 && (mailerData.queued_count ?? 0) > 0 && /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: [
1352
+ mailerData.queued_count,
1353
+ "q"
1354
+ ] }),
1355
+ mailerData.total > 0 && (mailerData.queued_count ?? 0) > 0 && /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: [
1356
+ "+",
1357
+ mailerData.queued_count,
1358
+ "q"
1359
+ ] }),
1360
+ mailerData.failed > 0 && /* @__PURE__ */ u3("span", { class: "profiler-text--error profiler-text--xs", children: [
1361
+ "\u25B2 ",
1362
+ mailerData.failed
1363
+ ] })
1364
+ ]
1365
+ }
1366
+ ),
1286
1367
  envData && envData.total > 0 && /* @__PURE__ */ u3(
1287
1368
  ToolbarItem,
1288
1369
  {
@@ -4449,6 +4449,243 @@
4449
4449
  ] });
4450
4450
  }
4451
4451
 
4452
+ // app/assets/typescript/profiler/components/dashboard/tabs/MailerTab.tsx
4453
+ function BodyPreview({ email }) {
4454
+ const hasHtml = !!email.body_html;
4455
+ const hasText = !!email.body_text;
4456
+ if (!email.body_captured) {
4457
+ return /* @__PURE__ */ u3("div", { class: "profiler-mt-3 profiler-text--xs profiler-text--muted", children: [
4458
+ "Body not captured \u2014 enable ",
4459
+ /* @__PURE__ */ u3("code", { children: "config.capture_mail_body = true" }),
4460
+ " in your profiler initializer"
4461
+ ] });
4462
+ }
4463
+ if (!hasHtml && !hasText) return null;
4464
+ const [mode, setMode] = d2(hasHtml ? "preview" : "text");
4465
+ const switchMode = (m3) => (e3) => {
4466
+ e3.stopPropagation();
4467
+ setMode(m3);
4468
+ };
4469
+ const activeStyle = { borderColor: "var(--profiler-accent)", color: "var(--profiler-accent)" };
4470
+ return /* @__PURE__ */ u3("div", { class: "profiler-mt-3", children: [
4471
+ /* @__PURE__ */ u3("div", { class: "profiler-mb-2 profiler-flex profiler-flex--gap-2", style: { alignItems: "center" }, children: [
4472
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "Body" }),
4473
+ hasHtml && /* @__PURE__ */ u3(k, { children: [
4474
+ /* @__PURE__ */ u3("button", { class: "profiler-btn profiler-btn--sm", style: mode === "preview" ? activeStyle : {}, onClick: switchMode("preview"), children: "HTML preview" }),
4475
+ /* @__PURE__ */ u3("button", { class: "profiler-btn profiler-btn--sm", style: mode === "source" ? activeStyle : {}, onClick: switchMode("source"), children: "HTML source" })
4476
+ ] }),
4477
+ hasText && /* @__PURE__ */ u3("button", { class: "profiler-btn profiler-btn--sm", style: mode === "text" ? activeStyle : {}, onClick: switchMode("text"), children: "Plain text" })
4478
+ ] }),
4479
+ mode === "preview" && hasHtml && /* @__PURE__ */ u3(
4480
+ "iframe",
4481
+ {
4482
+ srcdoc: email.body_html,
4483
+ sandbox: "allow-same-origin",
4484
+ style: { width: "100%", height: "300px", border: "1px solid var(--profiler-border)", borderRadius: "var(--profiler-radius-md)", background: "#fff", display: "block" }
4485
+ }
4486
+ ),
4487
+ (mode === "source" || mode === "text") && /* @__PURE__ */ u3("pre", { style: { maxHeight: "300px", overflow: "auto", background: "var(--profiler-bg-lighter, rgba(0,0,0,0.2))", padding: "12px", borderRadius: "var(--profiler-radius-md)", fontSize: "12px", margin: 0, whiteSpace: "pre-wrap", wordBreak: "break-all", border: "1px solid var(--profiler-border)" }, children: mode === "source" ? email.body_html : email.body_text })
4488
+ ] });
4489
+ }
4490
+ function AssignsSection({ assigns }) {
4491
+ const entries = Object.entries(assigns);
4492
+ if (entries.length === 0) return null;
4493
+ return /* @__PURE__ */ u3("div", { class: "profiler-mt-3", children: [
4494
+ /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted profiler-mb-1", style: { textTransform: "uppercase", letterSpacing: "0.05em" }, children: "Variables" }),
4495
+ entries.map(([key, value]) => /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4496
+ /* @__PURE__ */ u3("span", { children: /* @__PURE__ */ u3("code", { children: [
4497
+ "@",
4498
+ key
4499
+ ] }) }),
4500
+ /* @__PURE__ */ u3("code", { style: { textAlign: "right", wordBreak: "break-all", maxWidth: "60%" }, children: value })
4501
+ ] }, key))
4502
+ ] });
4503
+ }
4504
+ function EmailDetail({ email }) {
4505
+ return /* @__PURE__ */ u3("div", { class: "profiler-mt-3", onClick: (e3) => e3.stopPropagation(), children: [
4506
+ email.subject && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4507
+ /* @__PURE__ */ u3("span", { children: "Subject" }),
4508
+ /* @__PURE__ */ u3("span", { children: email.subject })
4509
+ ] }),
4510
+ email.to && email.to.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4511
+ /* @__PURE__ */ u3("span", { children: "To" }),
4512
+ /* @__PURE__ */ u3("span", { style: { textAlign: "right", wordBreak: "break-all" }, children: email.to.join(", ") })
4513
+ ] }),
4514
+ email.from && email.from.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4515
+ /* @__PURE__ */ u3("span", { children: "From" }),
4516
+ /* @__PURE__ */ u3("span", { style: { textAlign: "right" }, children: email.from.join(", ") })
4517
+ ] }),
4518
+ email.cc && email.cc.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4519
+ /* @__PURE__ */ u3("span", { children: "CC" }),
4520
+ /* @__PURE__ */ u3("span", { style: { textAlign: "right" }, children: email.cc.join(", ") })
4521
+ ] }),
4522
+ email.bcc && email.bcc.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4523
+ /* @__PURE__ */ u3("span", { children: "BCC" }),
4524
+ /* @__PURE__ */ u3("span", { style: { textAlign: "right" }, children: email.bcc.join(", ") })
4525
+ ] }),
4526
+ email.reply_to && email.reply_to.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4527
+ /* @__PURE__ */ u3("span", { children: "Reply-To" }),
4528
+ /* @__PURE__ */ u3("span", { style: { textAlign: "right" }, children: email.reply_to.join(", ") })
4529
+ ] }),
4530
+ email.template && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4531
+ /* @__PURE__ */ u3("span", { children: "Template" }),
4532
+ /* @__PURE__ */ u3("code", { children: email.template })
4533
+ ] }),
4534
+ email.parts && email.parts.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4535
+ /* @__PURE__ */ u3("span", { children: "Parts" }),
4536
+ /* @__PURE__ */ u3("span", { style: { textAlign: "right" }, children: email.parts.join(", ") })
4537
+ ] }),
4538
+ email.attachments && email.attachments.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4539
+ /* @__PURE__ */ u3("span", { children: "Attachments" }),
4540
+ /* @__PURE__ */ u3("span", { style: { textAlign: "right" }, children: email.attachments.map((a3) => `${a3.filename} (${(a3.size / 1024).toFixed(1)} KB)`).join(", ") })
4541
+ ] }),
4542
+ /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4543
+ /* @__PURE__ */ u3("span", { children: "Render" }),
4544
+ /* @__PURE__ */ u3("span", { class: "profiler-query-card__duration", children: email.duration_ms != null ? `${email.duration_ms} ms` : "\u2014" })
4545
+ ] }),
4546
+ email.delivery_ms != null && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4547
+ /* @__PURE__ */ u3("span", { children: "Delivery" }),
4548
+ /* @__PURE__ */ u3("span", { class: "profiler-query-card__duration", children: [
4549
+ email.delivery_ms,
4550
+ " ms"
4551
+ ] })
4552
+ ] }),
4553
+ email.message_id && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4554
+ /* @__PURE__ */ u3("span", { children: "Message-ID" }),
4555
+ /* @__PURE__ */ u3("code", { style: { fontSize: "11px", wordBreak: "break-all", maxWidth: "70%", textAlign: "right" }, children: email.message_id })
4556
+ ] }),
4557
+ email.error && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
4558
+ /* @__PURE__ */ u3("span", { children: "Error" }),
4559
+ /* @__PURE__ */ u3("span", { class: "profiler-text--error", style: { textAlign: "right" }, children: email.error })
4560
+ ] }),
4561
+ email.assigns && Object.keys(email.assigns).length > 0 && /* @__PURE__ */ u3(AssignsSection, { assigns: email.assigns }),
4562
+ /* @__PURE__ */ u3(BodyPreview, { email })
4563
+ ] });
4564
+ }
4565
+ function EmailRow({ email, onClick, isExpanded }) {
4566
+ const cardClass = ["profiler-query-card", email.error ? "profiler-query-card--slow" : ""].filter(Boolean).join(" ");
4567
+ return /* @__PURE__ */ u3("div", { class: cardClass, style: { cursor: "pointer", marginBottom: "6px" }, onClick, children: [
4568
+ /* @__PURE__ */ u3("div", { class: "profiler-query-card__header", children: [
4569
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", style: { minWidth: 0, flex: 1 }, children: /* @__PURE__ */ u3("span", { class: "profiler-text--sm", style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
4570
+ /* @__PURE__ */ u3("strong", { children: email.mailer_class }),
4571
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: [
4572
+ "#",
4573
+ email.action
4574
+ ] }),
4575
+ email.subject && /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: [
4576
+ " \u2014 ",
4577
+ email.subject.slice(0, 60),
4578
+ email.subject.length > 60 ? "\u2026" : ""
4579
+ ] })
4580
+ ] }) }),
4581
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", style: { flexShrink: 0 }, children: [
4582
+ email.delivery_mode && /* @__PURE__ */ u3("span", { class: "badge-info profiler-text--xs", children: email.delivery_mode }),
4583
+ email.duration_ms != null && /* @__PURE__ */ u3("span", { class: "profiler-query-card__duration profiler-text--xs", children: [
4584
+ email.duration_ms,
4585
+ " ms"
4586
+ ] }),
4587
+ email.error ? /* @__PURE__ */ u3("span", { class: "badge-error profiler-text--xs", children: "Error" }) : /* @__PURE__ */ u3("span", { class: "badge-success profiler-text--xs", children: "Sent" }),
4588
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: isExpanded ? "\u25B2" : "\u25BC" })
4589
+ ] })
4590
+ ] }),
4591
+ isExpanded && /* @__PURE__ */ u3(EmailDetail, { email })
4592
+ ] });
4593
+ }
4594
+ function QueuedRow({ email }) {
4595
+ return /* @__PURE__ */ u3("div", { class: "profiler-query-card", style: { marginBottom: "6px", borderLeft: "3px solid var(--profiler-info, #38bdf8)" }, children: [
4596
+ /* @__PURE__ */ u3("div", { class: "profiler-query-card__header", children: [
4597
+ /* @__PURE__ */ u3("span", { class: "profiler-text--sm", children: [
4598
+ /* @__PURE__ */ u3("strong", { children: email.mailer_class }),
4599
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: [
4600
+ "#",
4601
+ email.action
4602
+ ] })
4603
+ ] }),
4604
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", style: { flexShrink: 0 }, children: [
4605
+ /* @__PURE__ */ u3("span", { class: "badge-info profiler-text--xs", children: "queued" }),
4606
+ email.delivery_method && /* @__PURE__ */ u3("span", { class: "badge-default profiler-text--xs", children: email.delivery_method }),
4607
+ email.duration_ms != null && /* @__PURE__ */ u3("span", { class: "profiler-query-card__duration profiler-text--xs", children: [
4608
+ email.duration_ms,
4609
+ " ms"
4610
+ ] })
4611
+ ] })
4612
+ ] }),
4613
+ email.assigns && Object.keys(email.assigns).length > 0 && /* @__PURE__ */ u3(AssignsSection, { assigns: email.assigns })
4614
+ ] });
4615
+ }
4616
+ function MailerTab({ mailerData }) {
4617
+ const [expandedIndex, setExpandedIndex] = d2(null);
4618
+ const [expandedErrorIndex, setExpandedErrorIndex] = d2(null);
4619
+ const queuedCount = mailerData?.queued_count ?? 0;
4620
+ if (!mailerData || mailerData.total === 0 && queuedCount === 0) {
4621
+ return /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "No emails sent during this request" }) });
4622
+ }
4623
+ const toggleRow = (i3) => setExpandedIndex(expandedIndex === i3 ? null : i3);
4624
+ const toggleErrorRow = (i3) => setExpandedErrorIndex(expandedErrorIndex === i3 ? null : i3);
4625
+ return /* @__PURE__ */ u3(k, { children: [
4626
+ /* @__PURE__ */ u3("h2", { class: "profiler-section__header", children: "Mailer Deliveries" }),
4627
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mb-4 profiler-text--sm", children: [
4628
+ /* @__PURE__ */ u3("span", { children: [
4629
+ "Total: ",
4630
+ /* @__PURE__ */ u3("strong", { children: mailerData.total })
4631
+ ] }),
4632
+ mailerData.deliver_now > 0 && /* @__PURE__ */ u3("span", { children: [
4633
+ "deliver_now: ",
4634
+ /* @__PURE__ */ u3("strong", { children: mailerData.deliver_now })
4635
+ ] }),
4636
+ mailerData.deliver_later > 0 && /* @__PURE__ */ u3("span", { children: [
4637
+ "deliver_later: ",
4638
+ /* @__PURE__ */ u3("strong", { children: mailerData.deliver_later })
4639
+ ] }),
4640
+ queuedCount > 0 && /* @__PURE__ */ u3("span", { children: [
4641
+ "queued: ",
4642
+ /* @__PURE__ */ u3("strong", { children: queuedCount })
4643
+ ] }),
4644
+ mailerData.multi_part_count > 0 && /* @__PURE__ */ u3("span", { children: [
4645
+ "Multi-part: ",
4646
+ /* @__PURE__ */ u3("strong", { children: mailerData.multi_part_count })
4647
+ ] }),
4648
+ mailerData.failed > 0 && /* @__PURE__ */ u3("span", { children: [
4649
+ "Errors: ",
4650
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--error", children: mailerData.failed })
4651
+ ] })
4652
+ ] }),
4653
+ mailerData.loop_warnings.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-alert-banner profiler-alert-banner--warning profiler-mb-4", children: [
4654
+ /* @__PURE__ */ u3("span", { class: "profiler-alert-banner__icon", children: "\u26A0\uFE0F" }),
4655
+ /* @__PURE__ */ u3("div", { style: { flex: 1 }, children: [
4656
+ /* @__PURE__ */ u3("strong", { children: "Send loop detected" }),
4657
+ " \u2014 ",
4658
+ mailerData.loop_warnings.length,
4659
+ " pattern",
4660
+ mailerData.loop_warnings.length > 1 ? "s" : "",
4661
+ mailerData.loop_warnings.map((w3, i3) => /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted profiler-mt-1", children: w3.message }, i3))
4662
+ ] })
4663
+ ] }),
4664
+ mailerData.truncated && /* @__PURE__ */ u3("div", { class: "profiler-alert-banner profiler-alert-banner--warning profiler-mb-4", children: [
4665
+ /* @__PURE__ */ u3("span", { class: "profiler-alert-banner__icon", children: "\u26A0\uFE0F" }),
4666
+ /* @__PURE__ */ u3("span", { children: "Showing first 50 emails only \u2014 additional emails were truncated" })
4667
+ ] }),
4668
+ mailerData.emails.length > 0 && /* @__PURE__ */ u3(k, { children: [
4669
+ /* @__PURE__ */ u3("h3", { class: "profiler-text--lg profiler-mt-4 profiler-mb-3", children: [
4670
+ "Emails",
4671
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--sm", style: { fontWeight: "normal", marginLeft: "8px" }, children: "click to expand" })
4672
+ ] }),
4673
+ mailerData.emails.map((email, i3) => /* @__PURE__ */ u3(EmailRow, { email, onClick: () => toggleRow(i3), isExpanded: expandedIndex === i3 }, i3))
4674
+ ] }),
4675
+ mailerData.queued && mailerData.queued.length > 0 && /* @__PURE__ */ u3(k, { children: [
4676
+ /* @__PURE__ */ u3("h3", { class: "profiler-text--lg profiler-mt-4 profiler-mb-2", children: [
4677
+ "Queued",
4678
+ /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--sm", style: { fontWeight: "normal", marginLeft: "8px" }, children: "deliver_later \u2014 will be sent in a background job" })
4679
+ ] }),
4680
+ mailerData.queued.map((email, i3) => /* @__PURE__ */ u3(QueuedRow, { email }, `q-${i3}`))
4681
+ ] }),
4682
+ mailerData.errors.length > 0 && /* @__PURE__ */ u3(k, { children: [
4683
+ /* @__PURE__ */ u3("h3", { class: "profiler-text--lg profiler-mt-6 profiler-mb-3 profiler-text--error", children: "Delivery Errors" }),
4684
+ mailerData.errors.map((email, i3) => /* @__PURE__ */ u3(EmailRow, { email, onClick: () => toggleErrorRow(i3), isExpanded: expandedErrorIndex === i3 }, `err-${i3}`))
4685
+ ] })
4686
+ ] });
4687
+ }
4688
+
4452
4689
  // app/assets/typescript/profiler/components/dashboard/ProfileDashboard.tsx
4453
4690
  function ProfileDashboard({ profile, initialTab, embedded }) {
4454
4691
  const cd = profile.collectors_data || {};
@@ -4459,6 +4696,7 @@
4459
4696
  const hasRoutes = (cd["routes"]?.total ?? 0) > 0;
4460
4697
  const hasI18n = (cd["i18n"]?.total ?? 0) > 0;
4461
4698
  const hasJobs = (profile.child_jobs?.length ?? 0) > 0;
4699
+ const hasMailers = (cd["mailer"]?.total ?? 0) > 0;
4462
4700
  const [activeTab, setActiveTab] = d2(hasException ? "exception" : initialTab);
4463
4701
  const handleTabClick = (tab) => (e3) => {
4464
4702
  e3.preventDefault();
@@ -4530,6 +4768,7 @@
4530
4768
  profile.child_jobs.length,
4531
4769
  ")"
4532
4770
  ] }),
4771
+ hasMailers && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("mailer"), onClick: handleTabClick("mailer"), children: "Mailers" }),
4533
4772
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" })
4534
4773
  ] }),
4535
4774
  /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
@@ -4546,6 +4785,7 @@
4546
4785
  activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
4547
4786
  activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] }),
4548
4787
  activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
4788
+ activeTab === "mailer" && /* @__PURE__ */ u3(MailerTab, { mailerData: cd["mailer"] }),
4549
4789
  activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"], readOnly: true })
4550
4790
  ] })
4551
4791
  ] }),
@@ -4615,7 +4855,8 @@
4615
4855
  const hasDumps = (cd["dump"]?.count ?? 0) > 0;
4616
4856
  const hasLogs = (cd["logs"]?.total ?? 0) > 0;
4617
4857
  const hasException = !!cd["exception"]?.exception_class;
4618
- const validTabs = ["job", "database", "cache", "http", "jobs", "dump", "logs", "exception", "env", "timeline"];
4858
+ const hasMailers = (cd["mailer"]?.total ?? 0) > 0;
4859
+ const validTabs = ["job", "database", "cache", "http", "jobs", "dump", "logs", "exception", "env", "timeline", "mailer"];
4619
4860
  const defaultTab = validTabs.includes(initialTab) ? initialTab : "job";
4620
4861
  const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
4621
4862
  const jobData = cd["job"];
@@ -4706,6 +4947,7 @@
4706
4947
  ")"
4707
4948
  ] }),
4708
4949
  hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
4950
+ hasMailers && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("mailer"), onClick: handleTabClick("mailer"), children: "Mailers" }),
4709
4951
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" })
4710
4952
  ] }),
4711
4953
  /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
@@ -4718,6 +4960,7 @@
4718
4960
  activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
4719
4961
  activeTab === "dump" && /* @__PURE__ */ u3(DumpsTab, { dumpData: cd["dump"] }),
4720
4962
  activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
4963
+ activeTab === "mailer" && /* @__PURE__ */ u3(MailerTab, { mailerData: cd["mailer"] }),
4721
4964
  activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"], readOnly: true })
4722
4965
  ] })
4723
4966
  ] }),
@@ -10,6 +10,7 @@ module Profiler
10
10
  super
11
11
  @events = []
12
12
  @subscriptions = []
13
+ @mutex = Mutex.new
13
14
  end
14
15
 
15
16
  def icon
@@ -38,7 +39,7 @@ module Profiler
38
39
 
39
40
  # Controller action
40
41
  @subscriptions << ActiveSupport::Notifications.monotonic_subscribe("process_action.action_controller") do |_name, started, finished, _unique_id, payload|
41
- @events << Models::TimelineEvent.new(
42
+ add_event Models::TimelineEvent.new(
42
43
  name: "#{payload[:controller]}##{payload[:action]}",
43
44
  started_at: started,
44
45
  finished_at: finished,
@@ -57,7 +58,7 @@ module Profiler
57
58
  # Template rendering
58
59
  @subscriptions << ActiveSupport::Notifications.monotonic_subscribe("render_template.action_view") do |_name, started, finished, _unique_id, payload|
59
60
  identifier = short_identifier(payload[:identifier])
60
- @events << Models::TimelineEvent.new(
61
+ add_event Models::TimelineEvent.new(
61
62
  name: "Render: #{identifier}",
62
63
  started_at: started,
63
64
  finished_at: finished,
@@ -69,7 +70,7 @@ module Profiler
69
70
  # Partial rendering
70
71
  @subscriptions << ActiveSupport::Notifications.monotonic_subscribe("render_partial.action_view") do |_name, started, finished, _unique_id, payload|
71
72
  identifier = short_identifier(payload[:identifier])
72
- @events << Models::TimelineEvent.new(
73
+ add_event Models::TimelineEvent.new(
73
74
  name: "Partial: #{identifier}",
74
75
  started_at: started,
75
76
  finished_at: finished,
@@ -84,7 +85,7 @@ module Profiler
84
85
  next if payload[:sql] =~ /^(BEGIN|COMMIT|ROLLBACK|SAVEPOINT)/i
85
86
 
86
87
  sql = payload[:sql].to_s
87
- @events << Models::TimelineEvent.new(
88
+ add_event Models::TimelineEvent.new(
88
89
  name: sql.length > 80 ? "#{sql[0, 80]}..." : sql,
89
90
  started_at: started,
90
91
  finished_at: finished,
@@ -98,7 +99,7 @@ module Profiler
98
99
  @subscriptions << ActiveSupport::Notifications.monotonic_subscribe(event_name) do |name, started, finished, _unique_id, payload|
99
100
  op = name.split(".").first.sub("cache_", "")
100
101
  key = payload[:key].to_s
101
- @events << Models::TimelineEvent.new(
102
+ add_event Models::TimelineEvent.new(
102
103
  name: "cache_#{op}: #{key.length > 60 ? "#{key[0, 60]}..." : key}",
103
104
  started_at: started,
104
105
  finished_at: finished,
@@ -111,7 +112,7 @@ module Profiler
111
112
 
112
113
  # Called by Profiler.measure to record custom instrumentation events
113
114
  def record_custom_event(label:, started_at:, finished_at:, metadata: {})
114
- @events << Models::TimelineEvent.new(
115
+ add_event Models::TimelineEvent.new(
115
116
  name: label,
116
117
  started_at: started_at,
117
118
  finished_at: finished_at,
@@ -122,7 +123,7 @@ module Profiler
122
123
 
123
124
  # Called by NetHttpInstrumentation to record outbound HTTP events
124
125
  def record_http_event(started_at:, finished_at:, url:, method:, status:)
125
- @events << Models::TimelineEvent.new(
126
+ add_event Models::TimelineEvent.new(
126
127
  name: "HTTP #{method} #{url}",
127
128
  started_at: started_at,
128
129
  finished_at: finished_at,
@@ -182,6 +183,10 @@ module Profiler
182
183
  roots
183
184
  end
184
185
 
186
+ def add_event(event)
187
+ @mutex.synchronize { @events << event }
188
+ end
189
+
185
190
  def short_identifier(identifier)
186
191
  return identifier.to_s unless identifier.to_s.include?("/")
187
192
 
@@ -9,6 +9,8 @@ module Profiler
9
9
  def initialize(profile)
10
10
  super
11
11
  @requests = []
12
+ @mutex = Mutex.new
13
+ @collected = false
12
14
  end
13
15
 
14
16
  def icon
@@ -40,47 +42,88 @@ module Profiler
40
42
  def collect
41
43
  Thread.current[:profiler_http_collector] = nil
42
44
 
43
- threshold = Profiler.configuration.slow_http_threshold
45
+ data = @mutex.synchronize do
46
+ @collected = true
47
+ build_data(@requests)
48
+ end
49
+ store_data(data)
50
+ end
44
51
 
45
- store_data(
46
- total_requests: @requests.size,
47
- total_duration: @requests.sum { |r| r[:duration] }.round(2),
48
- slow_requests: @requests.count { |r| r[:duration] >= threshold },
49
- error_requests: @requests.count { |r| r[:status] >= 400 || r[:status] == 0 },
50
- by_host: group_by_host,
51
- by_status: group_by_status,
52
- requests: @requests.map { |r| r.transform_keys(&:to_s) }
53
- )
52
+ # Called from NetHttpInstrumentation before the actual HTTP call.
53
+ # Returns the mutable entry so the caller can update it on completion.
54
+ def register_pending(payload)
55
+ entry = payload.merge(in_flight: true, status: 0, duration: nil,
56
+ response_headers: {}, response_body: nil,
57
+ response_body_encoding: "text", response_size: 0)
58
+ @mutex.synchronize { @requests << entry }
59
+ save_if_collected
60
+ entry
54
61
  end
55
62
 
56
- def record_request(payload)
57
- @requests << payload
63
+ # Called from NetHttpInstrumentation after the HTTP response is received.
64
+ def complete_request(entry, **data)
65
+ @mutex.synchronize { entry.merge!(data.merge(in_flight: false)) }
66
+ save_if_collected
67
+ end
68
+
69
+ # Called from NetHttpInstrumentation when the HTTP call raises.
70
+ def fail_request(entry, error:, duration:)
71
+ @mutex.synchronize { entry.merge!(in_flight: false, status: 0, duration: duration, error: error) }
72
+ save_if_collected
58
73
  end
59
74
 
60
75
  def toolbar_summary
61
- total = @requests.size
76
+ requests = @mutex.synchronize { @requests.dup }
77
+ total = requests.size
62
78
  return { text: "0 HTTP", color: "green" } if total == 0
63
79
 
64
80
  threshold = Profiler.configuration.slow_http_threshold
65
- errors = @requests.count { |r| r[:status] >= 400 || r[:status] == 0 }
66
- slow = @requests.count { |r| r[:duration] >= threshold }
67
- duration = @requests.sum { |r| r[:duration] }.round(2)
81
+ in_flight = requests.count { |r| r[:in_flight] }
82
+ errors = requests.count { |r| !r[:in_flight] && (r[:status] >= 400 || r[:status] == 0) }
83
+ slow = requests.count { |r| !r[:in_flight] && r[:duration] && r[:duration] >= threshold }
84
+ duration = requests.sum { |r| r[:duration].to_f }.round(2)
68
85
 
69
86
  color = if errors > 0 || slow > 0
70
87
  "red"
71
- elsif total > 10
88
+ elsif in_flight > 0 || total > 10
72
89
  "orange"
73
90
  else
74
91
  "green"
75
92
  end
76
93
 
77
- { text: "#{total} HTTP (#{duration}ms)", color: color }
94
+ text = in_flight > 0 ? "#{total} HTTP (#{in_flight} pending, #{duration}ms)" : "#{total} HTTP (#{duration}ms)"
95
+ { text: text, color: color }
78
96
  end
79
97
 
80
98
  private
81
99
 
82
- def group_by_host
83
- @requests.each_with_object(Hash.new(0)) do |req, h|
100
+ def build_data(requests)
101
+ threshold = Profiler.configuration.slow_http_threshold
102
+ {
103
+ total_requests: requests.size,
104
+ total_duration: requests.sum { |r| r[:duration].to_f }.round(2),
105
+ slow_requests: requests.count { |r| !r[:in_flight] && r[:duration] && r[:duration] >= threshold },
106
+ error_requests: requests.count { |r| !r[:in_flight] && (r[:status] >= 400 || r[:status] == 0) },
107
+ by_host: group_by_host(requests),
108
+ by_status: group_by_status(requests),
109
+ requests: requests.map { |r| r.transform_keys(&:to_s) }
110
+ }
111
+ end
112
+
113
+ # Rebuilds and persists HTTP data after collect has already run.
114
+ # Called when fire-and-forget threads register or complete requests post-collect.
115
+ def save_if_collected
116
+ data = @mutex.synchronize do
117
+ return unless @collected
118
+
119
+ build_data(@requests)
120
+ end
121
+ store_data(data)
122
+ Profiler.storage.save(@profile.token, @profile)
123
+ end
124
+
125
+ def group_by_host(requests)
126
+ requests.each_with_object(Hash.new(0)) do |req, h|
84
127
  host = begin
85
128
  URI.parse(req[:url]).host || "unknown"
86
129
  rescue URI::InvalidURIError
@@ -90,16 +133,17 @@ module Profiler
90
133
  end
91
134
  end
92
135
 
93
- def group_by_status
94
- @requests.each_with_object(Hash.new(0)) do |req, h|
95
- status = req[:status]
96
- key = if status == 0
136
+ def group_by_status(requests)
137
+ requests.each_with_object(Hash.new(0)) do |req, h|
138
+ key = if req[:in_flight]
139
+ "pending"
140
+ elsif req[:status] == 0
97
141
  "error"
98
- elsif status < 300
142
+ elsif req[:status] < 300
99
143
  "2xx"
100
- elsif status < 400
144
+ elsif req[:status] < 400
101
145
  "3xx"
102
- elsif status < 500
146
+ elsif req[:status] < 500
103
147
  "4xx"
104
148
  else
105
149
  "5xx"