Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9c44b45
Add transaction activity logs with expandable timeline view
cursoragent Jul 4, 2025
1b2b3fa
Checkpoint before follow-up message
cursoragent Jul 4, 2025
6858fba
Checkpoint before follow-up message
cursoragent Jul 4, 2025
d5df1ec
Checkpoint before follow-up message
cursoragent Jul 4, 2025
933aa1d
Refactor server client creation and add fallback for missing secret key
cursoragent Jul 4, 2025
a29b8c7
feat: add activity log section to transaction details page
cursoragent Jul 5, 2025
707076d
feat: improve activity log visual indicators and timestamp handling
cursoragent Jul 5, 2025
29310fb
fix: use same timestamp pattern as Timing Information card
cursoragent Jul 5, 2025
ad64fb9
feat: use createdAt instead of timestamp for activity log time displays
cursoragent Jul 5, 2025
9a42ff8
fix: correct event type matching for red dot color
cursoragent Jul 5, 2025
6f18324
feat: sort activity logs chronologically (oldest first)
cursoragent Jul 5, 2025
e82de0c
Refactor activity log entry UI with improved event type styling
cursoragent Jul 5, 2025
17dc805
Checkpoint before follow-up message
cursoragent Jul 5, 2025
c6c969f
Fix indentation in transaction details error handling logic
cursoragent Jul 5, 2025
0ece641
Refactor event type colors to use Badge component with variants
cursoragent Jul 5, 2025
ecc5479
Refactor activity log rendering with explicit sorting and rendering l…
cursoragent Jul 5, 2025
ccc3837
Refactor activity log sorting to use insertion sort algorithm
cursoragent Jul 5, 2025
3a44bd7
Checkpoint before follow-up message
cursoragent Jul 5, 2025
91a8db8
Remove timestamp from activity log entry item
cursoragent Jul 5, 2025
b106e3d
revert robots.txt change
joaquim-verges Jul 5, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ export async function getTransactionsChart({

// TODO - need to handle this error state, like we do with the connect charts
throw new Error(
`Error fetching transactions chart data: ${response.status} ${response.statusText} - ${await response.text().catch(() => "Unknown error")}`,
`Error fetching transactions chart data: ${response.status} ${
response.statusText
} - ${await response.text().catch(() => "Unknown error")}`,
);
}

Expand Down Expand Up @@ -192,11 +194,87 @@ export async function getSingleTransaction({

// TODO - need to handle this error state, like we do with the connect charts
throw new Error(
`Error fetching single transaction data: ${response.status} ${response.statusText} - ${await response.text().catch(() => "Unknown error")}`,
`Error fetching single transaction data: ${response.status} ${
response.statusText
} - ${await response.text().catch(() => "Unknown error")}`,
);
}

const data = (await response.json()).result as TransactionsResponse;

return data.transactions[0];
}

// Activity log types
export type ActivityLogEntry = {
id: string;
transactionId: string;
batchIndex: number;
eventType: string;
stageName: string;
executorName: string;
notificationId: string;
payload: Record<string, unknown> | string | number | boolean | null;
timestamp: string;
createdAt: string;
};

type ActivityLogsResponse = {
result: {
activityLogs: ActivityLogEntry[];
transaction: {
id: string;
batchIndex: number;
clientId: string;
};
pagination: {
totalCount: number;
page: number;
limit: number;
};
};
};

export async function getTransactionActivityLogs({
teamId,
clientId,
transactionId,
}: {
teamId: string;
clientId: string;
transactionId: string;
}): Promise<ActivityLogEntry[]> {
const authToken = await getAuthToken();

const response = await fetch(
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/activity-logs?transactionId=${transactionId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": clientId,
"x-team-id": teamId,
},
method: "GET",
},
);

if (!response.ok) {
if (response.status === 401) {
return [];
}

// Don't throw on 404 - activity logs might not exist for all transactions
if (response.status === 404) {
return [];
}

console.error(
`Error fetching activity logs: ${response.status} ${response.statusText}`,
);
return [];
}

const data = (await response.json()) as ActivityLogsResponse;
return data.result.activityLogs;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { notFound, redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/projects";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getSingleTransaction } from "../../lib/analytics";
import {
getSingleTransaction,
getTransactionActivityLogs,
} from "../../lib/analytics";
import { TransactionDetailsUI } from "./transaction-details-ui";

export default async function TransactionPage({
Expand All @@ -26,11 +29,18 @@ export default async function TransactionPage({
redirect(`/team/${team_slug}`);
}

const transactionData = await getSingleTransaction({
clientId: project.publishableKey,
teamId: project.teamId,
transactionId: id,
});
const [transactionData, activityLogs] = await Promise.all([
getSingleTransaction({
clientId: project.publishableKey,
teamId: project.teamId,
transactionId: id,
}),
getTransactionActivityLogs({
clientId: project.publishableKey,
teamId: project.teamId,
transactionId: id,
}),
]);

const client = getClientThirdwebClient({
jwt: authToken,
Expand All @@ -48,6 +58,7 @@ export default async function TransactionPage({
project={project}
teamSlug={team_slug}
transaction={transactionData}
activityLogs={activityLogs}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"use client";

import { format, formatDistanceToNowStrict } from "date-fns";
import { ExternalLinkIcon, InfoIcon } from "lucide-react";
import { ExternalLink, Info, ChevronDown, ChevronRight } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { hexToNumber, isHex, type ThirdwebClient, toEther } from "thirdweb";
import type { Project } from "@/api/projects";
import { WalletAddress } from "@/components/blocks/wallet-address";
Expand All @@ -16,15 +17,18 @@ import { useAllChainsData } from "@/hooks/chains/allChains";
import { ChainIconClient } from "@/icons/ChainIcon";
import { statusDetails } from "../../analytics/tx-table/tx-table-ui";
import type { Transaction } from "../../analytics/tx-table/types";
import { type ActivityLogEntry } from "../../lib/analytics";

export function TransactionDetailsUI({
transaction,
client,
activityLogs,
}: {
transaction: Transaction;
teamSlug: string;
client: ThirdwebClient;
project: Project;
activityLogs: ActivityLogEntry[];
}) {
const { idToChain } = useAllChainsData();

Expand All @@ -45,8 +49,8 @@ export function TransactionDetailsUI({
executionResult && "error" in executionResult
? executionResult.error.message
: executionResult && "revertData" in executionResult
? executionResult.revertData?.revertReason
: null;
? executionResult.revertData?.revertReason
: null;
const errorDetails =
executionResult && "error" in executionResult
? executionResult.error
Expand All @@ -68,12 +72,18 @@ export function TransactionDetailsUI({
// Gas information
const gasUsed =
executionResult && "actualGasUsed" in executionResult
? `${isHex(executionResult.actualGasUsed) ? hexToNumber(executionResult.actualGasUsed) : executionResult.actualGasUsed}`
? `${
isHex(executionResult.actualGasUsed)
? hexToNumber(executionResult.actualGasUsed)
: executionResult.actualGasUsed
}`
: "N/A";

const gasCost =
executionResult && "actualGasCost" in executionResult
? `${toEther(BigInt(executionResult.actualGasCost || "0"))} ${chain?.nativeCurrency.symbol || ""}`
? `${toEther(BigInt(executionResult.actualGasCost || "0"))} ${
chain?.nativeCurrency.symbol || ""
}`
: "N/A";

return (
Expand Down Expand Up @@ -156,7 +166,10 @@ export function TransactionDetailsUI({
rel="noopener noreferrer"
target="_blank"
>
{`${transactionHash.slice(0, 8)}...${transactionHash.slice(-6)}`}{" "}
{`${transactionHash.slice(
0,
8,
)}...${transactionHash.slice(-6)}`}{" "}
<ExternalLinkIcon className="size-4 text-muted-foreground" />
</Link>
</Button>
Expand All @@ -165,7 +178,10 @@ export function TransactionDetailsUI({
className="font-mono text-muted-foreground text-sm"
copyIconPosition="left"
textToCopy={transactionHash}
textToShow={`${transactionHash.slice(0, 6)}...${transactionHash.slice(-4)}`}
textToShow={`${transactionHash.slice(
0,
6,
)}...${transactionHash.slice(-4)}`}
tooltip="Copy transaction hash"
variant="ghost"
/>
Expand Down Expand Up @@ -347,7 +363,122 @@ export function TransactionDetailsUI({
)}
</CardContent>
</Card>

{/* Activity Log Card */}
<ActivityLogCard activityLogs={activityLogs} />
</div>
</>
);
}

// Activity Log Timeline Component
function ActivityLogCard({
activityLogs,
}: { activityLogs: ActivityLogEntry[] }) {
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Activity Log</CardTitle>
</CardHeader>
<CardContent>
{activityLogs.length === 0 ? (
<p className="text-muted-foreground text-sm">
No activity logs available for this transaction
</p>
) : (
<div className="space-y-4">
{activityLogs.map((log, index) => (
<ActivityLogEntry
key={log.id}
log={log}
isLast={index === activityLogs.length - 1}
/>
))}
</div>
)}
</CardContent>
</Card>
);
}

function ActivityLogEntry({
log,
isLast,
}: { log: ActivityLogEntry; isLast: boolean }) {
const [isExpanded, setIsExpanded] = useState(false);

return (
<div className="relative">
{/* Timeline line */}
{!isLast && (
<div className="absolute left-4 top-8 h-full w-0.5 bg-border" />
)}

<div className="flex items-start gap-4">
{/* Timeline dot */}
<div className="relative flex h-8 w-8 items-center justify-center rounded-full bg-muted">
<div className="h-3 w-3 rounded-full bg-primary" />
</div>

{/* Content */}
<div className="flex-1 min-w-0">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex w-full items-center justify-between py-2 text-left hover:bg-muted/50 rounded-md px-2 -ml-2"
>
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{log.stageName}</span>
<span className="text-muted-foreground text-xs">
{formatDistanceToNowStrict(new Date(log.timestamp), {
addSuffix: true,
})}
</span>
</div>
{isExpanded ? (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
</button>

{isExpanded && (
<div className="mt-2 space-y-3 px-2">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<div className="text-muted-foreground">Event Type</div>
<div className="font-mono">{log.eventType}</div>
</div>
<div>
<div className="text-muted-foreground">Executor</div>
<div className="font-mono">{log.executorName}</div>
</div>
<div>
<div className="text-muted-foreground">Batch Index</div>
<div className="font-mono">{log.batchIndex}</div>
</div>
<div>
<div className="text-muted-foreground">Timestamp</div>
<div className="font-mono text-xs">
{format(new Date(log.timestamp), "PP pp z")}
</div>
</div>
</div>

{log.payload && (
<div>
<div className="text-muted-foreground text-sm mb-2">
Payload
</div>
<CodeClient
code={JSON.stringify(log.payload, null, 2)}
lang="json"
/>
</div>
Comment on lines +538 to +542
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for payload JSON parsing

The JSON stringification could fail with circular references or other issues.

Add error handling:

{log.payload && (
  <div>
    <div className="text-muted-foreground text-sm mb-2">
      Payload
    </div>
-   <CodeClient
-     code={JSON.stringify(log.payload, null, 2)}
-     lang="json"
-   />
+   <CodeClient
+     code={(() => {
+       try {
+         return JSON.stringify(log.payload, null, 2);
+       } catch (error) {
+         return `Error displaying payload: ${error instanceof Error ? error.message : 'Unknown error'}`;
+       }
+     })()}
+     lang="json"
+   />
  </div>
)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<CodeClient
code={JSON.stringify(log.payload, null, 2)}
lang="json"
/>
</div>
{log.payload && (
<div>
<div className="text-muted-foreground text-sm mb-2">
Payload
</div>
<CodeClient
code={(() => {
try {
return JSON.stringify(log.payload, null, 2);
} catch (error) {
return `Error displaying payload: ${error instanceof Error ? error.message : 'Unknown error'}`;
}
})()}
lang="json"
/>
</div>
)}
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx
around lines 516 to 520, the JSON.stringify call on log.payload may throw errors
due to circular references or other issues. Wrap the JSON.stringify call in a
try-catch block to handle potential errors gracefully. In the catch block,
provide a fallback string or error message to display instead of crashing the
component.

)}
</div>
)}
</div>
</div>
</div>
);
}
Loading