Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fdd8e61
analytics: add Sentry Error Intelligence dashboard
builderio-bot May 12, 2026
129e25f
fix(analytics): handle Sentry API credential errors in dashboard
builderio-bot May 12, 2026
0350690
analytics: add Sentry org slug to required env vars
builderio-bot May 12, 2026
4e524ae
analytics: add Slack mention search for Sentry errors
builderio-bot May 12, 2026
354ca1d
fix(analytics): make SlackMentionsPanel interactive with mutation
builderio-bot May 12, 2026
ab1f6f5
analytics: add Slack user token support and AI issue classification
builderio-bot May 12, 2026
96e2fba
analytics: add multi-project filtering to Sentry errors dashboard
builderio-bot May 12, 2026
d569c81
slack: add fallback message search using bot token
builderio-bot May 12, 2026
25812a3
fix(analytics): remove private_channel type from conversations.list q…
builderio-bot May 12, 2026
a1b1d90
analytics: search Sentry issue URL in Slack mentions panel
builderio-bot May 12, 2026
fe1f398
fix: include trailing slash in Slack search query for URL matching
builderio-bot May 12, 2026
7f4e44b
debug: add logging to slack-messages search endpoint
builderio-bot May 12, 2026
6f19b83
fix: clarify Slack user token as optional with add button
builderio-bot May 12, 2026
6a0cd94
analytics: improve Slack channel access messaging with setup guide
builderio-bot May 12, 2026
5c0dd93
Add Slack User Token credential support to analytics template
builderio-bot May 12, 2026
b6348d5
analytics: resolve usernames and make Slack messages clickable
builderio-bot May 12, 2026
50e78bf
fix: resolve display name instead of falling back to user ID
builderio-bot May 12, 2026
256f561
analytics: limit Slack message search results to 10
builderio-bot May 12, 2026
0feaca6
analytics: add GitHub blame action and Sentry integration panel
builderio-bot May 12, 2026
7cb0734
remove GitHub blame panel from Sentry errors page
builderio-bot May 12, 2026
db032aa
slack: include thread replies in search results for context
builderio-bot May 12, 2026
bbfd0b5
fix: remove duplicate useState import in SlackMentionsPanel
builderio-bot May 12, 2026
6ab24c2
analytics: thread summarization + unified thread fetching
builderio-bot May 12, 2026
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
41 changes: 41 additions & 0 deletions templates/analytics/actions/github-blame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { defineAction } from "@agent-native/core";
import { z } from "zod";
import { getFileBlame } from "../server/lib/github";
import {
providerError,
requireActionCredentials,
} from "./_provider-action-utils";

export default defineAction({
description:
"Get Git blame for a file path in a GitHub repository. Returns the most recent commit per blame range, who authored it, and any associated PR.",
schema: z.object({
owner: z.string().describe("GitHub repository owner (org or user)"),
repo: z.string().describe("GitHub repository name"),
path: z.string().describe("File path within the repository"),
ref: z
.string()
.default("HEAD")
.describe("Branch, tag, or commit SHA (default: HEAD)"),
}),
readOnly: true,
run: async (args) => {
const credentials = await requireActionCredentials(
["GITHUB_TOKEN"],
"GitHub",
);
if (credentials.ok === false) return credentials.response;

try {
const result = await getFileBlame(
args.owner,
args.repo,
args.path,
args.ref,
);
return result;
} catch (err) {
return providerError(err);
}
},
});
15 changes: 14 additions & 1 deletion templates/analytics/actions/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from "zod";
import {
getIssueEvents,
getOrganizationStats,
getCodeMappings,
listOrganizations,
listIssues,
listProjects,
Expand All @@ -17,7 +18,14 @@ export default defineAction({
"Query Sentry projects, frequent issues, issue events, and organization error stats. Use this for Sentry error questions; pass statsPeriod like 7d, 30d, or 1y.",
schema: z.object({
mode: z
.enum(["organizations", "projects", "issues", "issue-events", "stats"])
.enum([
"organizations",
"projects",
"issues",
"issue-events",
"stats",
"code-mappings",
])
.default("issues")
.describe("What to query from Sentry"),
orgSlug: z
Expand Down Expand Up @@ -53,6 +61,11 @@ export default defineAction({
if (credentials.ok === false) return credentials.response;

try {
if (args.mode === "code-mappings") {
const mappings = await getCodeMappings(args.orgSlug);
return { mappings };
}

if (args.mode === "organizations") {
const organizations = await listOrganizations();
return { organizations, total: organizations.length };
Expand Down
2 changes: 1 addition & 1 deletion templates/analytics/actions/slack-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export default defineAction({

if (args.mode === "search") {
if (!args.query) return { error: "query is required" };
const result = await searchMessages(workspace, args.query);
const result = await searchMessages(workspace, args.query, args.limit);
const userIds = result.messages
.map((message) => message.user)
.filter((id): id is string => !!id);
Expand Down
22 changes: 20 additions & 2 deletions templates/analytics/app/lib/data-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ export const dataSources: DataSource[] = [
description: "Error tracking and performance monitoring",
category: "engineering",
icon: IconBug,
envKeys: ["SENTRY_AUTH_TOKEN"],
envKeys: ["SENTRY_AUTH_TOKEN", "SENTRY_ORG_SLUG"],
docsUrl: "https://docs.sentry.io/api/",
walkthroughSteps: [
{
Expand All @@ -566,6 +566,14 @@ export const dataSources: DataSource[] = [
inputPlaceholder: "sntrys_...",
inputType: "password",
},
{
title: "Enter your Organization Slug",
description:
"Your Sentry organization slug (e.g. my-company). Find it in your Sentry URL: sentry.io/organizations/<slug>/",
inputKey: "SENTRY_ORG_SLUG",
inputLabel: "Organization Slug",
inputPlaceholder: "my-company",
},
],
},
{
Expand Down Expand Up @@ -643,7 +651,7 @@ export const dataSources: DataSource[] = [
description: "Channel messages and workspace search",
category: "communication",
icon: IconMessage,
envKeys: ["SLACK_BOT_TOKEN"],
envKeys: ["SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"],
docsUrl: "https://api.slack.com/methods",
walkthroughSteps: [
{
Expand All @@ -666,6 +674,16 @@ export const dataSources: DataSource[] = [
inputPlaceholder: "xoxb-...",
inputType: "password",
},
{
title: "Add a User Token for search (optional)",
description:
"Optional. Enables the Slack search.messages API for richer search results. Without it, the bot scans channels it's been invited to.",
inputKey: "SLACK_USER_TOKEN",
inputLabel: "User Token (for search)",
inputPlaceholder: "xoxp-...",
inputType: "password",
optional: true,
},
],
},
{
Expand Down
12 changes: 8 additions & 4 deletions templates/analytics/app/pages/DataSources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -535,10 +535,14 @@ function ConnectedView({
Configured
</span>
) : optional ? (
<span className="flex items-center gap-1 whitespace-nowrap text-muted-foreground">
<IconCircle className="h-3 w-3" />
Optional
</span>
<button
type="button"
onClick={() => setEditing(true)}
className="flex items-center gap-1 whitespace-nowrap text-muted-foreground hover:text-primary text-xs cursor-pointer"
>
<IconPlus className="h-3 w-3" />
Add
</button>
) : (
<span className="flex items-center gap-1 whitespace-nowrap text-rose-400">
<IconAlertCircle className="h-3 w-3" />
Expand Down
13 changes: 12 additions & 1 deletion templates/analytics/app/pages/adhoc/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,17 @@ export interface DashboardMeta {

// Add new dashboards here. Each entry needs a matching file in this directory.
// REQUIRED FIELDS: id, name, author, lastUpdated
export const dashboards: DashboardMeta[] = [];
export const dashboards: DashboardMeta[] = [
{
id: "sentry-errors",
name: "Sentry Error Intelligence",
description:
"Top 10 errors, escalating issues, related error groups, and project breakdown",
author: "Liam DeBeasi",
lastUpdated: "2025-05-12",
dateCreated: "2025-05-12",
},
];

const HIDDEN_KEY = "hidden-dashboards";

Expand Down Expand Up @@ -143,6 +153,7 @@ export const dashboardComponents: Record<
> = {
explorer: lazy(() => import("./explorer")),
"explorer-dashboard": lazy(() => import("./explorer-dashboard")),
"sentry-errors": lazy(() => import("./sentry-errors")),
};

// Validate all dashboards at module load time
Expand Down
186 changes: 186 additions & 0 deletions templates/analytics/app/pages/adhoc/sentry-errors/ErrorGroupsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
IconChevronDown,
IconChevronRight,
IconExternalLink,
} from "@tabler/icons-react";
import type { SentryIssue } from "./index";

interface ErrorGroup {
type: string;
issues: SentryIssue[];
totalEvents: number;
totalUsers: number;
worstLevel: string;
}

function levelOrder(level: string): number {
return { fatal: 0, error: 1, warning: 2, info: 3, debug: 4 }[level] ?? 5;
}

function levelColor(level: string): string {
switch (level) {
case "fatal":
return "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400 border-red-200 dark:border-red-800";
case "error":
return "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400 border-orange-200 dark:border-orange-800";
case "warning":
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800";
default:
return "bg-muted text-muted-foreground border-border";
}
}

function formatCount(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
return String(n);
}

function timeAgo(date: string): string {
const ms = Date.now() - new Date(date).getTime();
const mins = Math.floor(ms / 60_000);
if (mins < 1) return "just now";
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
return `${days}d ago`;
}

function GroupRow({ group }: { group: ErrorGroup }) {
const [expanded, setExpanded] = useState(false);
return (
<div className="border-b border-border/50 last:border-0">
<button
type="button"
className="w-full text-left px-4 py-3 hover:bg-muted/40 transition-colors flex items-start gap-3"
onClick={() => setExpanded((v) => !v)}
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Badge
className={`text-[10px] px-1.5 py-0 ${levelColor(group.worstLevel)} border`}
>
{group.worstLevel}
</Badge>
<span className="text-xs text-muted-foreground">
{group.issues.length} issue{group.issues.length !== 1 ? "s" : ""}
</span>
</div>
<p className="text-sm font-medium mt-1 truncate">{group.type}</p>
</div>
<div className="shrink-0 text-right">
<p className="text-sm font-semibold tabular-nums">
{formatCount(group.totalEvents)}
</p>
<p className="text-xs text-muted-foreground">events</p>
</div>
<div className="shrink-0 pt-0.5">
{expanded ? (
<IconChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<IconChevronRight className="h-4 w-4 text-muted-foreground" />
)}
</div>
</button>

{expanded && (
<div className="bg-muted/20 border-t border-border/50">
{group.issues.map((issue) => (
<a
key={issue.id}
href={issue.permalink}
target="_blank"
rel="noopener noreferrer"
className="flex items-start gap-3 px-6 py-2.5 hover:bg-muted/50 transition-colors group"
>
<Badge
className={`text-[10px] px-1.5 py-0 shrink-0 mt-0.5 ${levelColor(issue.level)} border`}
>
{issue.level}
</Badge>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium truncate">
{issue.metadata.value ?? issue.title}
</p>
<p className="text-[10px] text-muted-foreground mt-0.5">
{issue.project.name} · {timeAgo(issue.lastSeen)} ·{" "}
{formatCount(parseInt(issue.count, 10))} events
</p>
</div>
<IconExternalLink className="h-3.5 w-3.5 text-muted-foreground shrink-0 mt-0.5 opacity-0 group-hover:opacity-100 transition-opacity" />
</a>
))}
</div>
)}
</div>
);
}

interface ErrorGroupsPanelProps {
issues: SentryIssue[];
isLoading: boolean;
}

export function ErrorGroupsPanel({ issues, isLoading }: ErrorGroupsPanelProps) {
const groups = useMemo((): ErrorGroup[] => {
const map = new Map<string, SentryIssue[]>();
for (const issue of issues) {
const type = issue.metadata.type ?? issue.type ?? "Unknown";
const arr = map.get(type) ?? [];
arr.push(issue);
map.set(type, arr);
}
return [...map.entries()]
.map(([type, issueList]) => {
const totalEvents = issueList.reduce(
(s, i) => s + parseInt(i.count, 10),
0,
);
const totalUsers = issueList.reduce((s, i) => s + i.userCount, 0);
const worstLevel = issueList.reduce((worst, i) => {
return levelOrder(i.level) < levelOrder(worst) ? i.level : worst;
}, "debug");
return { type, issues: issueList, totalEvents, totalUsers, worstLevel };
})
.sort((a, b) => b.totalEvents - a.totalEvents);
}, [issues]);

if (isLoading) {
return (
<div className="divide-y divide-border/50">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="px-4 py-3 flex gap-3">
<div className="flex-1 space-y-2">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-4 w-2/3" />
</div>
<Skeleton className="h-6 w-10 shrink-0" />
</div>
))}
</div>
);
}

if (!groups.length) {
return (
<div className="py-16 text-center">
<p className="text-sm text-muted-foreground">No error groups found</p>
</div>
);
}

return (
<ScrollArea className="h-[520px]">
<div>
{groups.map((group) => (
<GroupRow key={group.type} group={group} />
))}
</div>
</ScrollArea>
);
}
Loading