diff --git a/.gitignore b/.gitignore index 08ee14b..595c16c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ node_modules /public/build /public/entry.worker.js /build -server.js +/server/index.js /app/styles/tailwind.css /.idea diff --git a/app/config/config.server.ts b/app/config/config.server.ts index f47526f..f0e8542 100644 --- a/app/config/config.server.ts +++ b/app/config/config.server.ts @@ -36,6 +36,7 @@ invariant( typeof process.env.WEB_PUSH_VAPID_PUBLIC_KEY === "string", `Please define the "WEB_PUSH_VAPID_PUBLIC_KEY" environment variable`, ); +invariant(typeof process.env.SENTRY_DSN === "string", `Please define the "SENTRY_DSN" environment variable`); export default { app: { @@ -54,6 +55,9 @@ export default { url: process.env.REDIS_URL, password: process.env.REDIS_PASSWORD, }, + sentry: { + dsn: process.env.SENTRY_DSN, + }, twilio: { authToken: process.env.TWILIO_AUTH_TOKEN, }, diff --git a/app/entry.client.tsx b/app/entry.client.tsx index 13fa3dc..4c7672c 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -1,5 +1,21 @@ import { hydrate } from "react-dom"; import { RemixBrowser } from "@remix-run/react"; +import * as Sentry from "@sentry/browser"; +import { Integrations } from "@sentry/tracing"; + +declare global { + interface Window { + shellphoneConfig: { + sentry: { dsn: string }; + }; + } +} + +Sentry.init({ + dsn: window.shellphoneConfig.sentry.dsn, + tracesSampleRate: 1.0, + integrations: [new Integrations.BrowserTracing()], +}); hydrate(<RemixBrowser />, document); diff --git a/app/root.tsx b/app/root.tsx index c10cc97..e27b06a 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,17 +1,48 @@ import type { FunctionComponent, PropsWithChildren } from "react"; -import type { LinksFunction } from "@remix-run/node"; -import { Link, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useCatch } from "@remix-run/react"; +import { type LinksFunction, type LoaderFunction, json } from "@remix-run/node"; +import { + Link, + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useCatch, + useLoaderData, +} from "@remix-run/react"; +import config from "~/config/config.server"; import Logo from "~/features/core/components/logo"; import styles from "./styles/tailwind.css"; export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }]; +type LoaderData = { + shellphoneConfig: string; +}; +export const loader: LoaderFunction = () => { + return json<LoaderData>({ + shellphoneConfig: JSON.stringify({ + sentry: { + dsn: config.sentry.dsn, + }, + }), + }); +}; + export default function App() { + const { shellphoneConfig } = useLoaderData<LoaderData>(); return ( <Document> <Outlet /> + <script + suppressHydrationWarning + dangerouslySetInnerHTML={{ + __html: `window.shellphoneConfig=${shellphoneConfig};`, + }} + /> </Document> ); } diff --git a/app/routes/__app.tsx b/app/routes/__app.tsx index 5b07879..f8e5d1a 100644 --- a/app/routes/__app.tsx +++ b/app/routes/__app.tsx @@ -1,5 +1,6 @@ import { type LinksFunction, type LoaderFunction, json } from "@remix-run/node"; -import { Outlet, useCatch, useMatches } from "@remix-run/react"; +import { Outlet, useCatch, useLoaderData, useMatches } from "@remix-run/react"; +import * as Sentry from "@sentry/browser"; import serverConfig from "~/config/config.server"; import { type SessionData, requireLoggedIn } from "~/utils/auth.server"; @@ -10,6 +11,7 @@ import useServiceWorkerRevalidate from "~/features/core/hooks/use-service-worker import useDevice from "~/features/phone-calls/hooks/use-device"; import footerStyles from "~/features/core/components/footer.css"; import appStyles from "~/styles/app.css"; +import { useEffect } from "react"; export const links: LinksFunction = () => [ { rel: "stylesheet", href: appStyles }, @@ -35,9 +37,14 @@ export const loader: LoaderFunction = async ({ request }) => { export default function __App() { useDevice(); useServiceWorkerRevalidate(); + const { sessionData } = useLoaderData<AppLoaderData>(); const matches = useMatches(); const hideFooter = matches.some((match) => match.handle?.hideFooter === true); + useEffect(() => { + Sentry.setUser(sessionData.user); + }, []); + return ( <> <div className="h-full w-full overflow-hidden fixed bg-gray-100"> diff --git a/package-lock.json b/package-lock.json index 37818c9..dc7333f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,9 @@ "@remix-run/express": "1.5.1", "@remix-run/node": "1.5.1", "@remix-run/react": "1.5.1", + "@sentry/browser": "7.3.0", + "@sentry/node": "7.3.0", + "@sentry/tracing": "7.3.0", "@tailwindcss/forms": "0.5.2", "@tailwindcss/line-clamp": "0.4.0", "@tailwindcss/typography": "0.5.2", @@ -46,6 +49,7 @@ "tiny-invariant": "1.2.0", "tslog": "3.3.3", "twilio": "3.77.1", + "uuid": "8.3.2", "web-push": "3.5.0", "zod": "3.17.3" }, @@ -66,6 +70,7 @@ "@types/react": "18.0.10", "@types/react-dom": "18.0.5", "@types/secure-password": "3.1.1", + "@types/uuid": "8.3.4", "@types/web-push": "3.3.2", "@vitejs/plugin-react": "1.3.2", "c8": "7.11.3", @@ -2533,15 +2538,6 @@ "node": ">=0.6" } }, - "node_modules/@cypress/request/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@cypress/xvfb": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", @@ -3994,6 +3990,99 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/@sentry/browser": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.3.0.tgz", + "integrity": "sha512-UJMTDbajKNRGrs4ZQNelrPDaATvSZ9uELpPOtPSG6JUvB1BCwGgsgzz55RS0Uqs7B8KhMnDQ0kIn3FMewM4FMg==", + "dependencies": { + "@sentry/core": "7.3.0", + "@sentry/types": "7.3.0", + "@sentry/utils": "7.3.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.3.0.tgz", + "integrity": "sha512-EvuWVlYm0F0+BtIEmQiCL31Fw0cfKSwUTmxc99wvouaabpHBr2zCJHRxaXOWzxS705bYBJEQiFDTIHfoOQZMzA==", + "dependencies": { + "@sentry/hub": "7.3.0", + "@sentry/types": "7.3.0", + "@sentry/utils": "7.3.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/hub": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-7.3.0.tgz", + "integrity": "sha512-0GtTaWf/hoAMoIFY7Ke6eozIbG3FdIPM364sER4SxUQVSklp6AORrV6p82IgWPROK6aj83cPk9Bszgi6RiF/BA==", + "dependencies": { + "@sentry/types": "7.3.0", + "@sentry/utils": "7.3.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.3.0.tgz", + "integrity": "sha512-hPqLQMdpL9MeirtKDCgy0ekptWh58CCUhvcoQ2bYstVBsyMMTNTbiQqF/ClzanrScQ5CEuGWnCbKxrFN/S6cQg==", + "dependencies": { + "@sentry/core": "7.3.0", + "@sentry/hub": "7.3.0", + "@sentry/types": "7.3.0", + "@sentry/utils": "7.3.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/tracing": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.3.0.tgz", + "integrity": "sha512-A+mLEH8jtLkhfyw81EZA1XgI96jh9TIwH9EST3hdfSPgdZQf0A5sV8oVVh/d9Hw7NVb65Va5KhAZDNhcx5QxUA==", + "dependencies": { + "@sentry/hub": "7.3.0", + "@sentry/types": "7.3.0", + "@sentry/utils": "7.3.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/types": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.3.0.tgz", + "integrity": "sha512-cGkHdh9+uvbFTj65TjWcXuhe6vQiMY+U+N2GE5xCfmZT9hwuouCASViNsbJMpZqvCg+Yi0fasQLZ71rujiRNOA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.3.0.tgz", + "integrity": "sha512-xUP8TBf2p/c6CN8eFQ7Y+xk0IFrJXsph5ScozqNl/2l/Xs8hd2EiYETqgUklphoYD4J2RxvPwMyqBL15QN6wNg==", + "dependencies": { + "@sentry/types": "7.3.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -4627,6 +4716,12 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "node_modules/@types/web-push": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.3.2.tgz", @@ -5463,6 +5558,14 @@ "node": ">= 10.0.0" } }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -6094,14 +6197,6 @@ "node": ">=6" } }, - "node_modules/bullmq/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -13723,6 +13818,11 @@ "node": ">=8" } }, + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -17677,14 +17777,6 @@ "node": ">=10" } }, - "node_modules/preview-email/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/prisma": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.14.0.tgz", @@ -18760,14 +18852,6 @@ "@remix-run/server-runtime": "^1.0.0" } }, - "node_modules/remix-auth/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/remix-seo": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/remix-seo/-/remix-seo-0.1.0.tgz", @@ -18811,14 +18895,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/remix-utils/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/repeat-element": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", @@ -21849,9 +21925,9 @@ } }, "node_modules/uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "bin": { "uuid": "dist/bin/uuid" } @@ -24448,12 +24524,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true } } }, @@ -25482,6 +25552,78 @@ } } }, + "@sentry/browser": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.3.0.tgz", + "integrity": "sha512-UJMTDbajKNRGrs4ZQNelrPDaATvSZ9uELpPOtPSG6JUvB1BCwGgsgzz55RS0Uqs7B8KhMnDQ0kIn3FMewM4FMg==", + "requires": { + "@sentry/core": "7.3.0", + "@sentry/types": "7.3.0", + "@sentry/utils": "7.3.0", + "tslib": "^1.9.3" + } + }, + "@sentry/core": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.3.0.tgz", + "integrity": "sha512-EvuWVlYm0F0+BtIEmQiCL31Fw0cfKSwUTmxc99wvouaabpHBr2zCJHRxaXOWzxS705bYBJEQiFDTIHfoOQZMzA==", + "requires": { + "@sentry/hub": "7.3.0", + "@sentry/types": "7.3.0", + "@sentry/utils": "7.3.0", + "tslib": "^1.9.3" + } + }, + "@sentry/hub": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-7.3.0.tgz", + "integrity": "sha512-0GtTaWf/hoAMoIFY7Ke6eozIbG3FdIPM364sER4SxUQVSklp6AORrV6p82IgWPROK6aj83cPk9Bszgi6RiF/BA==", + "requires": { + "@sentry/types": "7.3.0", + "@sentry/utils": "7.3.0", + "tslib": "^1.9.3" + } + }, + "@sentry/node": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.3.0.tgz", + "integrity": "sha512-hPqLQMdpL9MeirtKDCgy0ekptWh58CCUhvcoQ2bYstVBsyMMTNTbiQqF/ClzanrScQ5CEuGWnCbKxrFN/S6cQg==", + "requires": { + "@sentry/core": "7.3.0", + "@sentry/hub": "7.3.0", + "@sentry/types": "7.3.0", + "@sentry/utils": "7.3.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + } + }, + "@sentry/tracing": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.3.0.tgz", + "integrity": "sha512-A+mLEH8jtLkhfyw81EZA1XgI96jh9TIwH9EST3hdfSPgdZQf0A5sV8oVVh/d9Hw7NVb65Va5KhAZDNhcx5QxUA==", + "requires": { + "@sentry/hub": "7.3.0", + "@sentry/types": "7.3.0", + "@sentry/utils": "7.3.0", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.3.0.tgz", + "integrity": "sha512-cGkHdh9+uvbFTj65TjWcXuhe6vQiMY+U+N2GE5xCfmZT9hwuouCASViNsbJMpZqvCg+Yi0fasQLZ71rujiRNOA==" + }, + "@sentry/utils": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.3.0.tgz", + "integrity": "sha512-xUP8TBf2p/c6CN8eFQ7Y+xk0IFrJXsph5ScozqNl/2l/Xs8hd2EiYETqgUklphoYD4J2RxvPwMyqBL15QN6wNg==", + "requires": { + "@sentry/types": "7.3.0", + "tslib": "^1.9.3" + } + }, "@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -26054,6 +26196,12 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "@types/web-push": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.3.2.tgz", @@ -26623,6 +26771,13 @@ "url": "0.10.3", "uuid": "8.0.0", "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + } } }, "aws-sign2": { @@ -27131,11 +27286,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" } } }, @@ -32775,6 +32925,11 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, + "lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -35565,13 +35720,6 @@ "open": "7", "pug": "^3.0.2", "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - } } }, "prisma": { @@ -36434,13 +36582,6 @@ "integrity": "sha512-VtzkfxeXbnXilupRTZkP40aik4vFSdwwRT96mbq0UBDMqHVRfQ7h9Y51HFrTufHJZEfAdkCopedMVvm0vQYKag==", "requires": { "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - } } }, "remix-auth-form": { @@ -36475,11 +36616,6 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==" - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" } } }, @@ -38914,9 +39050,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "uvu": { "version": "0.5.3", diff --git a/package.json b/package.json index 3bc3d61..2f27570 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev:build": "NODE_ENV=development dotenv npm run build:server -- -- --watch", "dev:css": "NODE_ENV=development tailwindcss -i ./styles/tailwind.css -o ./app/styles/tailwind.css --watch", "dev:remix": "NODE_ENV=development remix watch", - "dev:server": "NODE_ENV=development dotenv node ./server.js", + "dev:server": "NODE_ENV=development dotenv node ./server/index.js", "dev:worker": "NODE_ENV=development npm run build:worker -- --watch", "dev:init": "NODE_ENV=development dotenv run-s build:remix build:server", "dev": "npm run dev:init && run-p dev:build dev:worker dev:css dev:remix dev:server", @@ -15,7 +15,7 @@ "build:remix": "remix build", "build:worker": "node ./scripts/build-worker.js", "build": "NODE_ENV=production run-s build:css build:remix build:worker build:server", - "start": "NODE_ENV=production node ./server.js", + "start": "NODE_ENV=production node ./server/index.js", "test": "vitest", "test:coverage": "vitest run --coverage", "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", @@ -57,6 +57,9 @@ "@remix-run/express": "1.5.1", "@remix-run/node": "1.5.1", "@remix-run/react": "1.5.1", + "@sentry/browser": "7.3.0", + "@sentry/node": "7.3.0", + "@sentry/tracing": "7.3.0", "@tailwindcss/forms": "0.5.2", "@tailwindcss/line-clamp": "0.4.0", "@tailwindcss/typography": "0.5.2", @@ -89,6 +92,7 @@ "tiny-invariant": "1.2.0", "tslog": "3.3.3", "twilio": "3.77.1", + "uuid": "8.3.2", "web-push": "3.5.0", "zod": "3.17.3" }, @@ -109,6 +113,7 @@ "@types/react": "18.0.10", "@types/react-dom": "18.0.5", "@types/secure-password": "3.1.1", + "@types/uuid": "8.3.4", "@types/web-push": "3.3.2", "@vitejs/plugin-react": "1.3.2", "c8": "7.11.3", diff --git a/scripts/build-server.js b/scripts/build-server.js index 951309c..a2568e4 100644 --- a/scripts/build-server.js +++ b/scripts/build-server.js @@ -9,8 +9,8 @@ const watch = args.includes("--watch"); esbuild .build({ write: true, - outfile: path.join(basePath, "server.js"), - entryPoints: [path.join(basePath, "server.ts")], + outfile: path.join(basePath, "server/index.js"), + entryPoints: [path.join(basePath, "server/index.ts")], platform: "node", format: "cjs", bundle: true, @@ -19,7 +19,7 @@ esbuild { name: "remix-bundle-external", setup(build) { - build.onResolve({ filter: /^\.\/build$/ }, () => ({ external: true })); + build.onResolve({ filter: /^\.\.\/build$/ }, () => ({ external: true })); }, }, ], @@ -37,7 +37,7 @@ esbuild process.exit(1); } - console.log("Server rebuilt successfully"); + console.log("Server rebuilt successfully"); // TODO: find a way to restart the dev server process }, } : false, diff --git a/server.ts b/server/index.ts similarity index 61% rename from server.ts rename to server/index.ts index cecf59f..e86c1b2 100644 --- a/server.ts +++ b/server/index.ts @@ -1,18 +1,20 @@ -import path from "node:path"; -import express, { type NextFunction, type Request, type Response } from "express"; +import express from "express"; import compression from "compression"; import morgan from "morgan"; import { createRequestHandler } from "@remix-run/express"; -import { createBullBoard } from "@bull-board/api"; -import { BullMQAdapter } from "@bull-board/api/bullMQAdapter"; -import { ExpressAdapter } from "@bull-board/express"; -import { GlobalRole } from "@prisma/client"; +import * as Sentry from "@sentry/node"; -import cronJobs from "~/cron-jobs"; -import queues from "~/queues"; +import config from "~/config/config.server"; import logger from "~/utils/logger.server"; -import { __getSession } from "~/utils/session.server"; -import { type SessionData } from "~/utils/auth.server"; +import { adminMiddleware, setupBullBoard } from "./queues"; +import { registerSentry, sentryLoadContext } from "./sentry-remix"; + +Sentry.init({ + dsn: config.sentry.dsn, + integrations: [new Sentry.Integrations.Http({ tracing: true })], + tracesSampleRate: 1.0, + environment: process.env.NODE_ENV, +}); const app = express(); app.use((req, res, next) => { @@ -83,46 +85,21 @@ app.all("*", (req, res, next) => { } return createRequestHandler({ - build: require("./build"), + build: registerSentry(require("../build")), mode: process.env.NODE_ENV, + getLoadContext: sentryLoadContext, })(req, res, next); }); const port = process.env.PORT || 3000; app.listen(port, () => { - require("./build"); // preload the build so we're ready for the first request logger.info(`Server listening on port ${port}`); }); -async function adminMiddleware(req: Request, res: Response, next: NextFunction) { - const session = await __getSession(req.headers.cookie); - const sessionData: SessionData | undefined = session.data.user; - if (!sessionData || sessionData.user.role !== GlobalRole.SUPERADMIN) { - return res.setHeader("Location", "/sign-in").status(302).end(); - } - - next(); -} - -function setupBullBoard() { - const serverAdapter = new ExpressAdapter(); - const cronJobsQueues = registerCronJobs(); - createBullBoard({ - queues: [...queues, ...cronJobsQueues].map((queue) => new BullMQAdapter(queue)), - serverAdapter, - }); - serverAdapter.setBasePath("/admin/queues"); - return serverAdapter; -} - -function registerCronJobs() { - return cronJobs.map((registerCronJob) => registerCronJob()); -} - -const buildDir = path.join(process.cwd(), "build"); function purgeRequireCache() { + const resolved = require.resolve("../build"); for (const key in require.cache) { - if (key.startsWith(buildDir)) { + if (key.startsWith(resolved)) { delete require.cache[key]; } } diff --git a/server/queues.ts b/server/queues.ts new file mode 100644 index 0000000..f4a57e6 --- /dev/null +++ b/server/queues.ts @@ -0,0 +1,35 @@ +import type { NextFunction, Request, Response } from "express"; +import { ExpressAdapter } from "@bull-board/express"; +import { BullMQAdapter } from "@bull-board/api/bullMQAdapter"; +import { createBullBoard } from "@bull-board/api"; +import { GlobalRole } from "@prisma/client"; + +import { __getSession } from "~/utils/session.server"; +import type { SessionData } from "~/utils/auth.server"; +import queues from "~/queues"; +import cronJobs from "~/cron-jobs"; + +export async function adminMiddleware(req: Request, res: Response, next: NextFunction) { + const session = await __getSession(req.headers.cookie); + const sessionData: SessionData | undefined = session.data.user; + if (!sessionData || sessionData.user.role !== GlobalRole.SUPERADMIN) { + return res.setHeader("Location", "/sign-in").status(302).end(); + } + + next(); +} + +export function setupBullBoard() { + const serverAdapter = new ExpressAdapter(); + const cronJobsQueues = registerCronJobs(); + createBullBoard({ + queues: [...queues, ...cronJobsQueues].map((queue) => new BullMQAdapter(queue)), + serverAdapter, + }); + serverAdapter.setBasePath("/admin/queues"); + return serverAdapter; +} + +function registerCronJobs() { + return cronJobs.map((registerCronJob) => registerCronJob()); +} diff --git a/server/sentry-remix.ts b/server/sentry-remix.ts new file mode 100644 index 0000000..78693c6 --- /dev/null +++ b/server/sentry-remix.ts @@ -0,0 +1,109 @@ +import type { Request, Response } from "express"; +import type { ActionFunction, DataFunctionArgs, LoaderFunction, ServerBuild } from "@remix-run/node"; +import { isResponse } from "@remix-run/server-runtime/responses"; +import type { Transaction } from "@sentry/types"; +import * as Sentry from "@sentry/node"; +import { v4 as uuid } from "uuid"; + +import { __getSession } from "~/utils/session.server"; +import type { SessionData } from "~/utils/auth.server"; + +function wrapDataFunc(func: ActionFunction | LoaderFunction, routeId: string, method: string) { + const ogFunc = func; + + return async (args: DataFunctionArgs) => { + const session = await __getSession(args.request.headers.get("Cookie")); + const sessionData: SessionData | undefined = session.data.user; + if (sessionData) { + Sentry.setUser({ + id: sessionData.user.id, + email: sessionData.user.email, + role: sessionData.user.role, + }); + } else { + Sentry.configureScope((scope) => scope.setUser(null)); + } + + const parentTransaction: Transaction | undefined = args.context && args.context.__sentry_transaction; + const transaction = parentTransaction?.startChild({ + op: `${method}:${routeId}`, + description: `${method}: ${routeId}`, + }); + if (transaction) { + transaction.setStatus("ok"); + transaction.transaction = parentTransaction; + } + + try { + return await ogFunc(args); + } catch (error) { + if (isResponse(error)) { + throw error; + } + + Sentry.captureException(error, { + tags: { + global_id: parentTransaction && parentTransaction.tags["global_id"], + }, + }); + transaction?.setStatus("internal_error"); + throw error; + } finally { + transaction?.finish(); + } + }; +} + +// Register Sentry across your entire remix build. +export function registerSentry(build: ServerBuild) { + type Route = ServerBuild["routes"][string]; + + const routes: Record<string, Route> = {}; + + for (const [id, route] of Object.entries(build.routes)) { + const newRoute: Route = { ...route, module: { ...route.module } }; + + if (route.module.action) { + newRoute.module.action = wrapDataFunc(route.module.action, id, "action"); + } + + if (route.module.loader) { + newRoute.module.loader = wrapDataFunc(route.module.loader, id, "loader"); + } + + routes[id] = newRoute; + } + + return { + ...build, + routes, + }; +} + +export function sentryLoadContext(req: Request, res: Response) { + const transaction = Sentry.getCurrentHub().startTransaction({ + op: "request", + name: `${req.method}: ${req.url}`, + description: `${req.method}: ${req.url}`, + metadata: { + requestPath: req.url, + }, + tags: { + global_id: uuid(), + }, + }); + transaction && transaction.setStatus("internal_error"); + + res.once("finish", () => { + if (transaction) { + transaction.setHttpStatus(res.statusCode); + transaction.setTag("http.status_code", res.statusCode); + transaction.setTag("http.method", req.method); + transaction.finish(); + } + }); + + return { + __sentry_transaction: transaction, + }; +}