+
Events
-
);
diff --git a/apps/analytics/app/_components/analytics/EventsTable/StackTraceDialog.tsx b/apps/analytics/app/_components/analytics/EventsTable/StackTraceDialog.tsx
new file mode 100644
index 00000000..ed049cf2
--- /dev/null
+++ b/apps/analytics/app/_components/analytics/EventsTable/StackTraceDialog.tsx
@@ -0,0 +1,13 @@
+import { DialogButton } from "~/components/DialogButton";
+import { Event } from "~/db/getEvents";
+
+export function StackTraceDialog({ error }: { error: Event }) {
+ return (
+
+ );
+}
diff --git a/apps/analytics/app/_components/analytics/EventsTable/TableFilter.tsx b/apps/analytics/app/_components/analytics/EventsTable/TableFilter.tsx
new file mode 100644
index 00000000..191ebad4
--- /dev/null
+++ b/apps/analytics/app/_components/analytics/EventsTable/TableFilter.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useState, type Dispatch, type SetStateAction } from "react";
+import { Button } from "~/components/ui/button";
+import { Checkbox } from "~/components/ui/checkbox";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "~/components/ui/dropdown-menu";
+import { type EventType } from "./EventsTable";
+
+type TableFilterProps = {
+ eventTypes: EventType[];
+ setEventTypes: Dispatch
>;
+};
+
+const TableFilter = ({ eventTypes, setEventTypes }: TableFilterProps) => {
+ const [options, setOptions] = useState(eventTypes);
+
+ const toggleOption = (option: string) => {
+ setOptions((prevState) =>
+ prevState.map((t) =>
+ t.text === option ? { ...t, isSelected: !t.isSelected } : t
+ )
+ );
+ };
+
+ const toggleAllOptions = (isSelected: boolean) => {
+ setOptions((prevState) => prevState.map((t) => ({ ...t, isSelected })));
+ };
+
+ const isAllSelected = options.every((option) => option.isSelected);
+
+ return (
+
+
+
+
+
+ Select events
+
+
+
+
+
+
+ {options.map((option) => (
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default TableFilter;
diff --git a/apps/analytics/app/_components/analytics/cards/TotalDataExported.tsx b/apps/analytics/app/_components/analytics/cards/TotalDataExported.tsx
new file mode 100644
index 00000000..8325614d
--- /dev/null
+++ b/apps/analytics/app/_components/analytics/cards/TotalDataExported.tsx
@@ -0,0 +1,15 @@
+import { getTotalInterviewsStarted } from "~/utils/getTotalInterviewsStarted";
+import { SummaryCard } from "~/components/SummaryCard";
+
+const TotalDataExported = async () => {
+ const totalInterviewsStarted = await getTotalInterviewsStarted();
+ return (
+
+ );
+};
+
+export default TotalDataExported;
diff --git a/apps/analytics/app/_components/analytics/cards/TotalErrorsCard.tsx b/apps/analytics/app/_components/analytics/cards/TotalErrorsCard.tsx
new file mode 100644
index 00000000..fb0a3757
--- /dev/null
+++ b/apps/analytics/app/_components/analytics/cards/TotalErrorsCard.tsx
@@ -0,0 +1,15 @@
+import { SummaryCard } from "~/components/SummaryCard";
+import { getTotalErrors } from "~/utils/getTotalErrors";
+
+const TotalErrorsCard = async () => {
+ const totalErrors = await getTotalErrors();
+ return (
+
+ );
+};
+
+export default TotalErrorsCard;
diff --git a/apps/analytics/app/_components/errors/ErrorsTable/Columns.tsx b/apps/analytics/app/_components/errors/ErrorsTable/Columns.tsx
deleted file mode 100644
index accb6b11..00000000
--- a/apps/analytics/app/_components/errors/ErrorsTable/Columns.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-"use client";
-import { DataTableColumnHeader } from "~/components/DataTable/column-header";
-import { ColumnDef } from "@tanstack/react-table";
-import { Error } from "~/db/schema";
-import { StackTraceDialog } from "~/app/_components/errors/ErrorsTable/StackTraceDialog";
-export const columns: ColumnDef[] = [
- {
- accessorKey: "message",
- header: ({ column }) => (
-
- ),
- },
- {
- accessorKey: "details",
- header: ({ column }) => (
-
- ),
- },
- {
- accessorKey: "path",
- header: ({ column }) => (
-
- ),
- },
- {
- accessorKey: "installationid",
- header: ({ column }) => (
-
- ),
- },
- {
- accessorKey: "timestamp",
- header: ({ column }) => (
-
- ),
- },
- {
- accessorKey: "stacktrace",
- header: "",
- cell: ({ row }) => {
- return (
-
-
-
- );
- },
- },
-];
diff --git a/apps/analytics/app/_components/errors/ErrorsTable/ErrorsTable.tsx b/apps/analytics/app/_components/errors/ErrorsTable/ErrorsTable.tsx
deleted file mode 100644
index 449e07d9..00000000
--- a/apps/analytics/app/_components/errors/ErrorsTable/ErrorsTable.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { DataTable } from "~/components/DataTable/data-table";
-import getErrors from "~/db/getErrors";
-import { columns } from "./Columns";
-import ExportButton from "~/components/ExportButton";
-import { Card, CardHeader, CardContent } from "~/components/ui/card";
-
-export default async function ErrorsTable() {
- const errors = await getErrors();
-
- return (
- <>
-
-
- >
- );
-}
diff --git a/apps/analytics/app/_components/errors/ErrorsTable/StackTraceDialog.tsx b/apps/analytics/app/_components/errors/ErrorsTable/StackTraceDialog.tsx
deleted file mode 100644
index 8245f1f5..00000000
--- a/apps/analytics/app/_components/errors/ErrorsTable/StackTraceDialog.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { DialogButton } from "~/components/DialogButton";
-import type { Error } from "~/db/schema";
-
-export function StackTraceDialog({ error }: { error: Error }) {
- return (
-
- );
-}
diff --git a/apps/analytics/app/_components/errors/ErrorsView.tsx b/apps/analytics/app/_components/errors/ErrorsView.tsx
deleted file mode 100644
index 45d81e43..00000000
--- a/apps/analytics/app/_components/errors/ErrorsView.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import ErrorsTable from "./ErrorsTable/ErrorsTable";
-import TotalErrorsCard from "./cards/TotalErrorsCard";
-
-export default function ErrorsView() {
- return (
-
- );
-}
diff --git a/apps/analytics/app/_components/errors/cards/TotalErrorsCard.tsx b/apps/analytics/app/_components/errors/cards/TotalErrorsCard.tsx
deleted file mode 100644
index 50246c5a..00000000
--- a/apps/analytics/app/_components/errors/cards/TotalErrorsCard.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { SummaryCard } from "~/components/SummaryCard";
-import getErrors from "~/db/getErrors";
-
-const TotalErrorsCard = async () => {
- const errors = await getErrors();
- const totalErrors = errors.length;
- return ;
-};
-
-export default TotalErrorsCard;
diff --git a/apps/analytics/app/api/event/route.test.ts b/apps/analytics/app/api/event/route.test.ts
new file mode 100644
index 00000000..a95d815f
--- /dev/null
+++ b/apps/analytics/app/api/event/route.test.ts
@@ -0,0 +1,97 @@
+import { testApiHandler } from "next-test-api-route-handler";
+import insertEvent from "~/db/insertEvent";
+import * as appHandler from "./route";
+import { Event } from "@codaco/analytics";
+
+jest.mock("~/db/insertEvent", () => jest.fn());
+
+describe("/api/event", () => {
+ it("should insert event to the database", async () => {
+ const eventData = {
+ type: "AppSetup",
+ metadata: {
+ details: "testing details",
+ path: "testing path",
+ },
+ installationId: "21321546453213123",
+ timestamp: new Date().toString(),
+ };
+
+ (insertEvent as jest.Mock).mockImplementation(async (eventData: Event) => {
+ return { data: eventData, error: null };
+ });
+
+ await testApiHandler({
+ appHandler,
+ test: async ({ fetch }) => {
+ const response = await fetch({
+ method: "POST",
+ body: JSON.stringify(eventData),
+ });
+ expect(insertEvent).toHaveBeenCalledWith({
+ ...eventData,
+ timestamp: new Date(eventData.timestamp),
+ });
+ expect(response.status).toBe(200);
+ expect(await response.json()).toEqual({ event: eventData });
+ },
+ });
+ });
+
+ it("should return 400 if event is invalid", async () => {
+ const eventData = {
+ type: "InvalidEvent",
+ metadata: {
+ details: "testing details",
+ path: "testing path",
+ },
+ timestamp: new Date().toString(),
+ };
+
+ await testApiHandler({
+ appHandler,
+ test: async ({ fetch }) => {
+ const response = await fetch({
+ method: "POST",
+ body: JSON.stringify(eventData),
+ });
+ expect(response.status).toBe(400);
+ expect(await response.json()).toEqual({ error: "Invalid event" });
+ },
+ });
+ });
+
+ it("should return 500 if there is an error inserting the event to the database", async () => {
+ const eventData = {
+ type: "AppSetup",
+ metadata: {
+ details: "testing details",
+ path: "testing path",
+ },
+ installationId: "21321546453213123",
+ timestamp: new Date().toString(),
+ };
+
+ (insertEvent as jest.Mock).mockImplementation(async (eventData: Event) => {
+ return { data: null, error: "Error inserting events" };
+ });
+
+ await testApiHandler({
+ appHandler,
+ test: async ({ fetch }) => {
+ const response = await fetch({
+ method: "POST",
+ body: JSON.stringify(eventData),
+ });
+ expect(insertEvent).toHaveBeenCalledWith({
+ ...eventData,
+ timestamp: new Date(eventData.timestamp),
+ });
+ expect(response.status).toBe(500);
+ expect(await response.json()).toEqual({
+ error: "Error inserting events",
+ });
+ },
+ });
+ });
+});
diff --git a/apps/analytics/app/api/event/route.ts b/apps/analytics/app/api/event/route.ts
index efc7f23d..421fbf26 100644
--- a/apps/analytics/app/api/event/route.ts
+++ b/apps/analytics/app/api/event/route.ts
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
-import { sql } from "@vercel/postgres";
-import type { DispatchableAnalyticsEvent } from "@codaco/analytics";
+import insertEvent from "~/db/insertEvent";
+import { EventsSchema } from "@codaco/analytics";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
@@ -11,41 +11,32 @@ const corsHeaders = {
export const runtime = "edge";
export async function POST(request: NextRequest) {
- const event = (await request.json()) as DispatchableAnalyticsEvent;
-
- const timestamp = JSON.stringify(event.timestamp || new Date().toISOString());
-
- // determine if this is an error and push it to the errors table
- if (event.type === "Error") {
- const errorPayload = event.error;
- try {
- await sql`INSERT INTO Errors (message, details, stacktrace, timestamp, installationid, path) VALUES (${errorPayload.message}, ${errorPayload.details}, ${errorPayload.stacktrace}, ${timestamp}, ${event.installationId}, ${errorPayload.path});`;
- return NextResponse.json(
- { errorPayload },
- { status: 200, headers: corsHeaders }
- );
- } catch (error) {
- return NextResponse.json(
- { error },
- { status: 500, headers: corsHeaders }
- );
- }
+ const event = (await request.json()) as unknown;
+ const parsedEvent = EventsSchema.safeParse(event);
+
+ if (parsedEvent.success === false) {
+ return NextResponse.json(
+ { error: "Invalid event" },
+ { status: 400, headers: corsHeaders }
+ );
}
- // event is not an error
- // push the event to the events table
-
try {
- await sql`INSERT INTO EVENTS (type, metadata, timestamp, installationid, isocode) VALUES(
- ${event.type},
- ${JSON.stringify(event.metadata)},
- ${timestamp},
- ${event.installationId},
- ${event.geolocation?.countryCode}
- );`;
+ const result = await insertEvent({
+ ...parsedEvent.data,
+ timestamp: new Date(parsedEvent.data.timestamp),
+ message: parsedEvent.data.error?.message,
+ name: parsedEvent.data.error?.name,
+ stack: parsedEvent.data.error?.stack,
+ });
+ if (result.error) throw new Error(result.error);
+
return NextResponse.json({ event }, { status: 200, headers: corsHeaders });
} catch (error) {
- return NextResponse.json({ error }, { status: 500, headers: corsHeaders });
+ return NextResponse.json(
+ { error: "Error inserting events" },
+ { status: 500, headers: corsHeaders }
+ );
}
}
diff --git a/apps/analytics/app/layout.tsx b/apps/analytics/app/layout.tsx
index 28e0e802..821e92c8 100644
--- a/apps/analytics/app/layout.tsx
+++ b/apps/analytics/app/layout.tsx
@@ -6,8 +6,8 @@ import { ClerkProvider } from "@clerk/nextjs";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Fresco Analytics ",
+ description: "This is the analytics dashboard for Fresco.",
};
export default function RootLayout({
diff --git a/apps/analytics/app/page.tsx b/apps/analytics/app/page.tsx
index f327d961..74d54fbe 100644
--- a/apps/analytics/app/page.tsx
+++ b/apps/analytics/app/page.tsx
@@ -1,7 +1,5 @@
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
-import AnalyticsView from "~/app/_components/analytics/AnalyticsView";
-import ErrorsView from "~/app/_components/errors/ErrorsView";
import { UserButton } from "@clerk/nextjs";
+import AnalyticsView from "~/app/_components/analytics/AnalyticsView";
import UserManagementDialog from "./_components/users/UserManagementDialog";
export default function DashboardPage() {
@@ -16,18 +14,7 @@ export default function DashboardPage() {