Added posthog and sentry to days-since project

This commit is contained in:
Ceferino Patino 2024-11-30 17:05:08 -06:00
commit f37137d3b7
Signed by: c4patino
SSH key fingerprint: SHA256:Wu+cU1t+7zVT9wzew/4meNmeulo66NzMqc22MdDBgXI
17 changed files with 2225 additions and 95 deletions

3
.gitignore vendored
View file

@ -147,3 +147,6 @@ dist
# End of https://www.toptal.com/developers/gitignore/api/node,direnv
# Sentry Config File
.env.sentry-build-plugin

View file

@ -5,6 +5,74 @@
import "./src/env.js";
/** @type {import("next").NextConfig} */
const config = {};
const coreConfig = {
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
async rewrites() {
return [
{
source: "/ingest/static/:path*",
destination: "https://us-assets.i.posthog.com/static/:path*",
},
{
source: "/ingest/:path*",
destination: "https://us.i.posthog.com/:path*",
},
{
source: "/ingest/decide",
destination: "https://us.i.posthog.com/decide",
},
];
},
skipTrailingSlashRedirect: true,
};
import { withSentryConfig } from "@sentry/nextjs";
const config = withSentryConfig(coreConfig, {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: "c4thebomb",
project: "days-since",
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Automatically annotate React components to show their full name in breadcrumbs and session replay
reactComponentAnnotation: {
enabled: true,
},
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
});
export default config;

View file

@ -10,7 +10,7 @@
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"dev": "next dev --turbo",
"dev": "next dev",
"lint": "next lint",
"lint:fix": "next lint --fix",
"preview": "next build && next start",
@ -23,6 +23,7 @@
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@sentry/nextjs": "^8",
"@t3-oss/env-nextjs": "^0.10.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@ -31,6 +32,7 @@
"lucide-react": "^0.460.0",
"next": "^15.0.3",
"postgres": "^3.4.5",
"posthog-js": "^1.194.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"server-only": "^0.0.1",

1916
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

26
sentry.client.config.ts Normal file
View file

@ -0,0 +1,26 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://03edba4d067507bcde69b46755933f24@o4508389509758976.ingest.us.sentry.io/4508389512052736",
// Add optional integrations for additional features
integrations: [Sentry.replayIntegration()],
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Define how likely Replay events are sampled.
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// Define how likely Replay events are sampled when an error occurs.
replaysOnErrorSampleRate: 1.0,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

16
sentry.edge.config.ts Normal file
View file

@ -0,0 +1,16 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://03edba4d067507bcde69b46755933f24@o4508389509758976.ingest.us.sentry.io/4508389512052736",
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

15
sentry.server.config.ts Normal file
View file

@ -0,0 +1,15 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://03edba4d067507bcde69b46755933f24@o4508389509758976.ingest.us.sentry.io/4508389512052736",
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View file

@ -0,0 +1,14 @@
"use client";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
if (typeof window !== "undefined") {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: "/ingest",
});
}
export function CSPostHogProvider({ children }: { children: React.ReactNode }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}

View file

@ -2,7 +2,11 @@
import { useEffect, useState } from "react";
const formatTime = (elapsed: number): [number, number, number, number] => {
const formatTime = (elapsed: number | null) => {
if (elapsed === null) {
return null;
}
const seconds = Math.floor(elapsed / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
@ -12,10 +16,19 @@ const formatTime = (elapsed: number): [number, number, number, number] => {
const remainingMinutes = minutes % 60;
const remainingHours = hours % 24;
return [days, remainingHours, remainingMinutes, remainingSeconds];
return {
days,
hours: remainingHours,
mins: remainingMinutes,
secs: remainingSeconds,
};
};
const calculateTimeSince = (start: Date) => {
if (start === null) {
return null;
}
const now = new Date();
return now.getTime() - start.getTime();
};
@ -34,7 +47,7 @@ export default function Timer(props: { eventTime: Date; caption: string }) {
return () => clearInterval(interval);
}, [props.eventTime]);
const [days, hours, mins, secs] = formatTime(timeSince);
const formattedTime = formatTime(timeSince);
return (
<div className="flex h-full w-full flex-col justify-center">
@ -44,25 +57,33 @@ export default function Timer(props: { eventTime: Date; caption: string }) {
<div className="flex justify-center gap-20 py-12">
<div className="text-center">
<div className="text-8xl font-semibold">
{days.toString().padStart(2, "0")}
{formattedTime
? formattedTime.days.toString().padStart(2, "0")
: "00"}
</div>
<div className="text-md px-1 text-gray-400">DAYS</div>
</div>
<div className="text-center">
<div className="text-8xl font-semibold">
{hours.toString().padStart(2, "0")}
{formattedTime
? formattedTime.hours.toString().padStart(2, "0")
: "00"}
</div>
<div className="text-md px-1 text-gray-400">HOURS</div>
</div>
<div className="text-center">
<div className="text-8xl font-semibold">
{mins.toString().padStart(2, "0")}
{formattedTime
? formattedTime.mins.toString().padStart(2, "0")
: "00"}
</div>
<div className="text-md px-1 text-gray-400">MINUTES</div>
</div>
<div className="text-center">
<div className="text-8xl font-semibold" suppressHydrationWarning>
{secs.toString().padStart(2, "0")}
{formattedTime
? formattedTime.secs.toString().padStart(2, "0")
: "00"}
</div>
<div className="text-md px-1 text-gray-400">SECONDS</div>
</div>

View file

@ -8,9 +8,13 @@ import {
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
import { Avatar, AvatarImage } from "~/components/ui/avatar";
const formatTime = (elapsed: number | null) => {
if (elapsed === null) {
return null;
}
const formatTime = (elapsed: number): [number, number, number, number] => {
const seconds = Math.floor(elapsed / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
@ -20,16 +24,25 @@ const formatTime = (elapsed: number): [number, number, number, number] => {
const remainingMinutes = minutes % 60;
const remainingHours = hours % 24;
return [days, remainingHours, remainingMinutes, remainingSeconds];
return {
days,
hours: remainingHours,
mins: remainingMinutes,
secs: remainingSeconds,
};
};
const calculateTimeSince = (start: Date) => {
const calculateTimeSince = (start: Date | null) => {
if (start === null) {
return null;
}
const now = new Date();
return now.getTime() - start.getTime();
};
export default function Timer(props: {
eventTime: Date;
eventTime: Date | null;
caption?: string;
email?: string;
emailHash?: string;
@ -47,7 +60,7 @@ export default function Timer(props: {
return () => clearInterval(interval);
}, [props.eventTime]);
const [days, hours, mins, secs] = formatTime(timeSince);
const formattedTime = formatTime(timeSince);
return (
<Card className={"flex h-full w-full flex-col justify-center"}>
@ -64,25 +77,33 @@ export default function Timer(props: {
<div className="flex justify-center gap-8 py-10">
<div className="text-center">
<div className="text-6xl font-semibold">
{days.toString().padStart(2, "0")}
{formattedTime
? formattedTime?.days.toString().padStart(2, "0")
: "00"}
</div>
<div className="text-sm text-gray-400">DAYS</div>
</div>
<div className="text-center">
<div className="text-6xl font-semibold">
{hours.toString().padStart(2, "0")}
{formattedTime
? formattedTime?.hours.toString().padStart(2, "0")
: "00"}
</div>
<div className="text-sm text-gray-400">HOURS</div>
</div>
<div className="text-center">
<div className="text-6xl font-semibold">
{mins.toString().padStart(2, "0")}
{formattedTime
? formattedTime?.mins.toString().padStart(2, "0")
: "00"}
</div>
<div className="text-sm text-gray-400">MINUTES</div>
</div>
<div className="text-center">
<div className="text-6xl font-semibold" suppressHydrationWarning>
{secs.toString().padStart(2, "0")}
{formattedTime
? formattedTime?.secs.toString().padStart(2, "0")
: "00"}
</div>
<div className="text-sm text-gray-400">SECONDS</div>
</div>

View file

@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
// A faulty API route to test Sentry's error monitoring
export function GET() {
throw new Error("Sentry Example API Route Error");
return NextResponse.json({ data: "Testing Sentry Error..." });
}

19
src/app/global-error.tsx Normal file
View file

@ -0,0 +1,19 @@
"use client";
import * as Sentry from "@sentry/nextjs";
import NextError from "next/error";
import { useEffect } from "react";
export default function GlobalError(props: { error: unknown }) {
useEffect(() => {
Sentry.captureException(props.error);
}, [props.error]);
return (
<html>
<body>
<NextError statusCode={500} />
</body>
</html>
);
}

View file

@ -3,6 +3,7 @@ import "~/styles/globals.css";
import { Inter } from "next/font/google";
import { type Metadata } from "next";
import { TopNav } from "./_components/navigation";
import { CSPostHogProvider } from "./_analytics/providers";
export const metadata: Metadata = {
title: "Days Since",
@ -21,14 +22,16 @@ export default function RootLayout({
}: Readonly<{ children: React.ReactNode; modal: React.ReactNode }>) {
return (
<html lang="en">
<body className={`font-sans ${inter.variable} dark`}>
<div className="grid h-screen grid-rows-[auto,1fr]">
<TopNav />
<main className="grid items-start overflow-y-auto">{children}</main>
{modal}
<div id="modal-root" />
</div>
</body>
<CSPostHogProvider>
<body className={`font-sans ${inter.variable} dark`}>
<div className="grid h-screen grid-rows-[auto,1fr]">
<TopNav />
<main className="grid items-start overflow-y-auto">{children}</main>
{modal}
<div id="modal-root" />
</div>
</body>
</CSPostHogProvider>
</html>
);
}

View file

@ -1,8 +1,7 @@
import Timer from "~/app/_components/timer";
import { createHash } from "crypto";
import { getAllTimers } from "~/server/queries";
import Link from "next/link";
import Timer from "~/app/_components/timer";
import { getAllTimers } from "~/server/queries";
const hashEmail = (email: string) => {
const hash = createHash("sha256")
@ -15,11 +14,13 @@ const hashEmail = (email: string) => {
export default async function HomePage() {
const timers = await getAllTimers();
const modulo = timers.length > 4 ? Math.floor(Math.random() * 4) + 2 : 1;
return (
<div className={"m-4 grid grid-cols-4 gap-4"}>
{timers.map((timer, idx) =>
idx % 4 == 0 ? (
<div key={idx} className="row-span-2">
idx % modulo == 0 ? (
<div key={timer.id} className="row-span-2">
<Link href={`/timer/${timer.id}`}>
<Timer
eventTime={timer.updatedAt}
@ -30,15 +31,16 @@ export default async function HomePage() {
</Link>
</div>
) : (
<Link href={`/timer/${timer.id}`}>
<Timer
key={idx}
eventTime={timer.updatedAt}
caption={timer.caption}
email={timer.userEmail}
emailHash={hashEmail(timer.userEmail)}
/>
</Link>
<div key={timer.id} className="row-span-1">
<Link href={`/timer/${timer.id}`}>
<Timer
eventTime={timer.updatedAt}
caption={timer.caption}
email={timer.userEmail}
emailHash={hashEmail(timer.userEmail)}
/>
</Link>
</div>
),
)}
</div>

View file

@ -11,6 +11,7 @@ export const env = createEnv({
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
SENTRY_AUTH_TOKEN: z.string(),
},
/**
@ -19,7 +20,7 @@ export const env = createEnv({
* `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(),
NEXT_PUBLIC_POSTHOG_KEY: z.string(),
},
/**
@ -29,7 +30,8 @@ export const env = createEnv({
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially

13
src/instrumentation.ts Normal file
View file

@ -0,0 +1,13 @@
import * as Sentry from "@sentry/nextjs";
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("../sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("../sentry.edge.config");
}
}
export const onRequestError = Sentry.captureRequestError;

View file

@ -7,7 +7,6 @@ import {
timestamp,
varchar,
boolean,
uuid,
} from "drizzle-orm/pg-core";
/**
@ -28,9 +27,8 @@ export const timers = createTable("timer", {
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.$onUpdate(() => new Date())
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
() => new Date(),
),
public: boolean("public").default(false).notNull(),
});