Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/errors-page-polish-and-ga.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

Ship the Errors page to all users, with a polish + bug-fix pass: pinned "No channel" item in the Slack alert channel picker, viewer-timezone alert timestamps via Slack's `<!date^>` token, Activity sparkline peak tooltip, centered loading spinner and bug-icon empty state on the error detail page, ellipsis on the Configure alerts trigger.
11 changes: 10 additions & 1 deletion apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
Comment thread
D-K-P marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export function ConfigureErrorAlerts({
name={slackChannel.name}
placeholder={<span className="text-text-dimmed">Select a Slack channel</span>}
heading="Filter channels…"
defaultValue={selectedSlackChannelValue}
value={selectedSlackChannelValue ?? ""}
dropdownIcon
variant="tertiary/medium"
items={slack.channels}
Expand All @@ -218,6 +218,15 @@ export function ConfigureErrorAlerts({
>
{(matches) => (
<>
<SelectItem
value=""
className="border-b border-grid-bright text-text-dimmed"
>
<div className="flex items-center gap-1.5">
<XMarkIcon className="size-4" />
<span>No channel</span>
</div>
</SelectItem>
{matches?.map((channel) => (
<SelectItem
key={channel.id}
Expand Down
20 changes: 9 additions & 11 deletions apps/webapp/app/components/navigation/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -524,17 +524,15 @@ export function SideMenu({
isCollapsed={isCollapsed}
/>
)}
{(user.admin || user.isImpersonating) && (
<SideMenuItem
name="Errors"
icon={IconBugFilled}
activeIconColor="text-errors"
inactiveIconColor="text-errors"
to={v3ErrorsPath(organization, project, environment)}
data-action="errors"
isCollapsed={isCollapsed}
/>
)}
<SideMenuItem
name="Errors"
icon={IconBugFilled}
activeIconColor="text-errors"
inactiveIconColor="text-errors"
to={v3ErrorsPath(organization, project, environment)}
data-action="errors"
isCollapsed={isCollapsed}
/>
Comment thread
matt-aitken marked this conversation as resolved.
Comment thread
D-K-P marked this conversation as resolved.
<SideMenuItem
name="Query"
icon={TableCellsIcon}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { type LoaderFunctionArgs, type ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { type MetaFunction, useFetcher, useRevalidator } from "@remix-run/react";
import { BellAlertIcon } from "@heroicons/react/20/solid";
import { IconAlarmSnooze as IconAlarmSnoozeBase, IconCircleDotted } from "@tabler/icons-react";
import {
IconAlarmSnooze as IconAlarmSnoozeBase,
IconBugFilled,
IconCircleDotted,
} from "@tabler/icons-react";
import { parse } from "@conform-to/zod";
import { z } from "zod";
import { ErrorStatusBadge } from "~/components/errors/ErrorStatusBadge";
Expand All @@ -27,7 +31,7 @@ import {
import { type NextRunList } from "~/presenters/v3/NextRunListPresenter.server";
import { $replica } from "~/db.server";
import { logsClickhouseClient, clickhouseClient } from "~/services/clickhouseInstance.server";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { PageBody } from "~/components/layout/AppLayout";
import {
ResizableHandle,
Expand Down Expand Up @@ -324,13 +328,23 @@ export default function Page() {
}}
title={<span className="font-mono text-xs">{ErrorId.toFriendlyId(fingerprint)}</span>}
/>
<PageAccessories>
<LinkButton
to={alertsHref}
variant="secondary/small"
LeadingIcon={BellAlertIcon}
leadingIconClassName="text-alerts"
>
Configure alerts…
</LinkButton>
</PageAccessories>
</NavBar>

<PageBody scrollable={false}>
<Suspense
fallback={
<div className="my-2 flex items-center justify-center">
<div className="mx-auto flex items-center gap-2">
<div className="flex h-full items-center justify-center">
<div className="flex items-center gap-2">
<Spinner />
<Paragraph variant="small">Loading error details…</Paragraph>
</div>
Expand Down Expand Up @@ -366,7 +380,6 @@ export default function Page() {
projectParam={projectParam}
envParam={envParam}
fingerprint={fingerprint}
alertsHref={alertsHref}
/>
);
}}
Expand All @@ -385,7 +398,6 @@ function ErrorGroupDetail({
projectParam,
envParam,
fingerprint,
alertsHref,
}: {
errorGroup: ErrorGroupSummary | undefined;
runList: NextRunList | undefined;
Expand All @@ -394,7 +406,6 @@ function ErrorGroupDetail({
projectParam: string;
envParam: string;
fingerprint: string;
alertsHref: string;
}) {
const { value, values } = useSearchParams();
const organization = useOrganization();
Expand Down Expand Up @@ -499,9 +510,12 @@ function ErrorGroupDetail({
additionalTableState={{ errorId: ErrorId.toFriendlyId(fingerprint) }}
/>
) : (
<Paragraph variant="small" className="p-4 text-text-dimmed">
No runs found for this error.
</Paragraph>
<div className="flex flex-1 flex-col items-center justify-center gap-3">
<IconBugFilled className="size-16 text-charcoal-650" />
<Paragraph className="max-w-32 text-center text-text-dimmed">
No runs found for this error.
</Paragraph>
</div>
)}
</div>
</div>
Expand All @@ -510,11 +524,7 @@ function ErrorGroupDetail({
{/* Right-hand detail sidebar */}
<ResizableHandle id="error-detail-handle" />
<ResizablePanel id="error-detail" min="280px" default="380px" max="500px" isStaticAtRest>
<ErrorDetailSidebar
errorGroup={errorGroup}
fingerprint={fingerprint}
alertsHref={alertsHref}
/>
<ErrorDetailSidebar errorGroup={errorGroup} fingerprint={fingerprint} />
</ResizablePanel>
</ResizablePanelGroup>
);
Expand All @@ -523,24 +533,14 @@ function ErrorGroupDetail({
function ErrorDetailSidebar({
errorGroup,
fingerprint,
alertsHref,
}: {
errorGroup: ErrorGroupSummary;
fingerprint: string;
alertsHref: string;
}) {
return (
<div className="grid h-full grid-rows-[auto_1fr] overflow-hidden bg-background-bright">
<div className="flex items-center justify-between border-b border-grid-dimmed py-2 pl-3 pr-2">
<div className="border-b border-grid-dimmed px-3 py-2">
<Header2 className="truncate">Details</Header2>
<LinkButton
to={alertsHref}
variant="secondary/small"
LeadingIcon={BellAlertIcon}
leadingIconClassName="text-alerts"
>
Configure alerts
</LinkButton>
</div>
<div className="overflow-y-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
<div className="flex flex-col gap-4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
statusActionToastMessage,
} from "~/components/errors/ErrorStatusMenu";
import { useToast } from "~/components/primitives/Toast";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import TooltipPortal from "~/components/primitives/TooltipPortal";
import { appliedSummary, FilterMenuProvider, TimeFilter } from "~/components/runs/v3/SharedFilters";
import { $replica } from "~/db.server";
Expand Down Expand Up @@ -463,7 +464,7 @@ function FiltersBar({
LeadingIcon={BellAlertIcon}
leadingIconClassName="text-alerts"
>
Configure alerts
Configure alerts
</LinkButton>
{list && <ListPagination list={list} />}
</div>
Expand Down Expand Up @@ -706,9 +707,14 @@ function ErrorActivityGraph({ activity }: { activity: ErrorOccurrenceActivity })
</BarChart>
</ResponsiveContainer>
</div>
<span className="-mt-1 text-xxs tabular-nums text-text-dimmed">
{formatNumberCompact(maxCount)}
</span>
<SimpleTooltip
button={
<span className="-mt-1 text-xxs tabular-nums text-text-dimmed">
{formatNumberCompact(maxCount)}
</span>
}
content="Peak occurrences in a single time bucket"
/>
Comment on lines +710 to +717
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 SimpleTooltip introduces a nested inside a in the table cell

The new SimpleTooltip wrapping the peak occurrences number (errors._index/route.tsx:710-717) renders a <button> element (via TooltipTrigger at Tooltip.tsx:87-88). This button sits inside the TableCell which has to={errorPath} (errors._index/route.tsx:583), rendering its children inside a <Link> (i.e., <a> tag) at Table.tsx:323-330. This creates a <button> nested inside an <a>, which is invalid HTML per the spec (interactive content cannot be nested in other interactive content). In practice most browsers handle this gracefully for hover-only tooltips, but it can cause accessibility warnings and subtle click-propagation issues. Consider using asChild={true} on the SimpleTooltip or restructuring so the tooltip trigger isn't a <button>.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

</div>
);
}
Expand Down
22 changes: 13 additions & 9 deletions apps/webapp/app/v3/services/alerts/deliverAlert.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1182,15 +1182,19 @@ export class DeliverAlertService extends BaseService {
}

#formatTimestamp(date: Date): string {
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
}).format(date);
const unix = Math.floor(date.getTime() / 1000);
const fallback =
new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
timeZone: "UTC",
}).format(date) + " UTC";
return `<!date^${unix}^{date_short_pretty} {time_secs}|${fallback}>`;
}

#buildWebhookGitObject(git: GitMetaLinks | null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,15 +383,19 @@ export class DeliverErrorGroupAlertService {
}

#formatTimestamp(date: Date): string {
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
}).format(date);
const unix = Math.floor(date.getTime() / 1000);
const fallback =
new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
timeZone: "UTC",
}).format(date) + " UTC";
return `<!date^${unix}^{date_short_pretty} {time_secs}|${fallback}>`;
}
}

Expand Down
Loading