Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ We are currently in beta and trying to rollout to public slowly. If you're inter
- [x] Marketing email
- [x] SMTP support
- [x] Schedule API
- [x] API Logs
- [ ] Webhook support
- [ ] BYO AWS credentials

Expand All @@ -57,7 +58,7 @@ We're currently working on opening unsend for public beta.
- Join the [Discord server](https://discord.gg/BU8n8pJv8S) for any questions and getting to know to other community members.
- ⭐ the repository to help us raise awareness.
- Spread the word on Twitter.
- Fix or create [issues](https://github.com/unsend/unsend/issues), that are needed for the first production release.
- Fix or create [issues](https://github.com/unsend-dev/unsend/issues), that are needed for the first production release.

## Tech Stack

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- CreateTable
CREATE TABLE "ApiLog" (
"id" TEXT NOT NULL,
"method" TEXT NOT NULL,
"path" TEXT NOT NULL,
"status" INTEGER NOT NULL,
"duration" INTEGER NOT NULL,
"request" TEXT,
"response" TEXT,
"error" TEXT,
"teamId" INTEGER NOT NULL,
"apiKeyId" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "ApiLog_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "ApiLog_teamId_createdAt_idx" ON "ApiLog"("teamId", "createdAt" DESC);

-- AddForeignKey
ALTER TABLE "ApiLog" ADD CONSTRAINT "ApiLog_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ApiLog" ADD CONSTRAINT "ApiLog_apiKeyId_fkey" FOREIGN KEY ("apiKeyId") REFERENCES "ApiKey"("id") ON DELETE SET NULL ON UPDATE CASCADE;
22 changes: 22 additions & 0 deletions apps/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ model Team {
contactBooks ContactBook[]
campaigns Campaign[]
dailyEmailUsages DailyEmailUsage[]
ApiLog ApiLog[]
}

enum Role {
Expand Down Expand Up @@ -164,6 +165,7 @@ model ApiKey {
lastUsed DateTime?
teamId Int
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
ApiLog ApiLog[]
}

enum EmailStatus {
Expand Down Expand Up @@ -309,3 +311,23 @@ model DailyEmailUsage {

@@id([teamId, domainId, date, type])
}

model ApiLog {
id String @id @default(cuid())
method String
path String
status Int
duration Int // in milliseconds
request String? // Store request body
response String? // Store response data
error String? // Store error message if any
teamId Int
apiKeyId Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

team Team @relation(fields: [teamId], references: [id])
apiKey ApiKey? @relation(fields: [apiKeyId], references: [id])

@@index([teamId, createdAt(sort: Desc)])
}
135 changes: 135 additions & 0 deletions apps/web/src/app/(dashboard)/api-logs/api-log-details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"use client";

import { Separator } from "@unsend/ui/src/separator";
import { SheetHeader, SheetTitle } from "@unsend/ui/src/sheet";
import Spinner from "@unsend/ui/src/spinner";
import { formatDate, formatDistanceToNowStrict } from "date-fns";
import { api } from "~/trpc/react";
import { ApiLogStatus, ApiLogStatusBadge } from "./api-log-status-badge";

interface ApiLogDetailsProps {
logId: string;
}

export default function ApiLogDetails({ logId }: ApiLogDetailsProps) {
const logQuery = api.apiLogs.getLog.useQuery({ id: logId });

if (logQuery.isLoading) {
return (
<div className="flex justify-center items-center h-96">
<Spinner className="w-6 h-6" innerSvgClass="stroke-primary" />
</div>
);
}

if (!logQuery.data) {
return <div>Log not found</div>;
}

const log = logQuery.data;

return (
<div className="space-y-6">
<SheetHeader>
<SheetTitle>API Log Details</SheetTitle>
</SheetHeader>

<div className="space-y-4">
<div>
<label className="text-sm font-medium text-muted-foreground">
Path
</label>
<div className="mt-1 font-mono text-sm">{log.path}</div>
</div>

<div className="grid grid-cols-4 gap-4">
<div>
<label className="text-sm font-medium text-muted-foreground">
Method
</label>
<div className="mt-1">{log.method}</div>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
Status
</label>
<div className="mt-1">
<ApiLogStatusBadge status={log.status as ApiLogStatus} />
</div>
</div>
{log.apiKey && (
<div>
<label className="text-sm font-medium text-muted-foreground">
API Key
</label>
<div className="mt-1">{log.apiKey.name}</div>
</div>
)}
</div>

<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-muted-foreground">
Time
</label>
<div className="mt-1">
<span>
{`${formatDate(log.createdAt, "MMM dd, yyyy hh:mm:ss a")}`}
</span>
<br />
<span>
{`(${formatDistanceToNowStrict(new Date(log.createdAt), {
addSuffix: true,
})})`}
</span>
</div>
</div>

<div>
<label className="text-sm font-medium text-muted-foreground">
Duration
</label>
<div className="mt-1">{log.duration}ms</div>
</div>
</div>
</div>

<Separator />

<div className="space-y-4">
<div>
<label className="text-sm font-medium text-muted-foreground">
Request Body
</label>
<pre className="mt-2 p-4 bg-muted rounded-lg overflow-auto max-h-60 text-sm">
{log.request
? JSON.stringify(JSON.parse(log.request), null, 2)
: "No request body"}
</pre>
</div>

<div>
<label className="text-sm font-medium text-muted-foreground">
Response
</label>
<pre className="mt-2 p-4 bg-muted rounded-lg overflow-auto max-h-60 text-sm">
{log.response
? JSON.stringify(JSON.parse(log.response), null, 2)
: "No response body"}
</pre>
</div>

{log.error && (
<div>
<label className="text-sm font-medium text-muted-foreground text-red-500">
Error
</label>
<div className="mt-2 p-4 bg-red-50 text-red-900 rounded-lg text-sm">
{log.error}
</div>
</div>
)}
</div>
</div>
);
}
Loading