diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index d670dab47..41feed1f9 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
+const { withAxiom } = require('next-axiom');
const fs = require('fs');
const path = require('path');
const { version } = require('./package.json');
@@ -91,4 +92,4 @@ const config = {
},
};
-module.exports = config;
+module.exports = withAxiom(config);
diff --git a/apps/web/package.json b/apps/web/package.json
index 4f6617d1e..484659740 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -33,6 +33,7 @@
"micro": "^10.0.1",
"next": "14.0.3",
"next-auth": "4.24.5",
+ "next-axiom": "^1.1.1",
"next-plausible": "^3.10.1",
"next-themes": "^0.2.1",
"perfect-freehand": "^1.2.0",
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 7753e1e53..0f3d1607f 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -2,6 +2,7 @@ import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
+import { AxiomWebVitals } from 'next-axiom';
import { PublicEnvScript } from 'next-runtime-env';
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
@@ -71,6 +72,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
+
+
diff --git a/package-lock.json b/package-lock.json
index 3dc4e9776..78a99b7a4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -111,6 +111,7 @@
"micro": "^10.0.1",
"next": "14.0.3",
"next-auth": "4.24.5",
+ "next-axiom": "^1.1.1",
"next-plausible": "^3.10.1",
"next-themes": "^0.2.1",
"perfect-freehand": "^1.2.0",
@@ -16668,6 +16669,22 @@
}
}
},
+ "node_modules/next-axiom": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/next-axiom/-/next-axiom-1.1.1.tgz",
+ "integrity": "sha512-0r/TJ+/zetD+uDc7B+2E7WpC86hEtQ1U+DuWYrP/JNmUz+ZdPFbrZgzOSqaZ6TwYbXP56VVlPfYwq1YsKHTHYQ==",
+ "dependencies": {
+ "remeda": "^1.29.0",
+ "whatwg-fetch": "^3.6.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "next": ">=13.4",
+ "react": ">=18.0.0"
+ }
+ },
"node_modules/next-contentlayer": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/next-contentlayer/-/next-contentlayer-0.3.4.tgz",
@@ -22936,6 +22953,11 @@
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
+ "node_modules/whatwg-fetch": {
+ "version": "3.6.20",
+ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
+ "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="
+ },
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
diff --git a/packages/lib/server-only/document/find-document-audit-logs.ts b/packages/lib/server-only/document/find-document-audit-logs.ts
index 4f423ce8c..d0800a4d9 100644
--- a/packages/lib/server-only/document/find-document-audit-logs.ts
+++ b/packages/lib/server-only/document/find-document-audit-logs.ts
@@ -3,8 +3,11 @@ import { prisma } from '@documenso/prisma';
import type { DocumentAuditLog } from '@documenso/prisma/client';
import type { Prisma } from '@documenso/prisma/client';
+import { AppError } from '../../errors/app-error';
+import type { TDocumentAuditLog } from '../../types/document-audit-logs';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
+import { buildServerLogger } from '../../utils/logger';
export interface FindDocumentAuditLogsOptions {
userId: number;
@@ -97,7 +100,26 @@ export const findDocumentAuditLogs = async ({
let nextCursor: string | undefined = undefined;
- const parsedData = data.map((auditLog) => parseDocumentAuditLogData(auditLog));
+ let parsedData: TDocumentAuditLog[] = [];
+
+ try {
+ parsedData = data.map((auditLog) => parseDocumentAuditLogData(auditLog));
+ } catch (err) {
+ const error = AppError.parseError(err);
+
+ if (error.code === 'MIGRATION_REQUIRED') {
+ const logger = buildServerLogger();
+
+ logger.error('findDocumentAuditLogs', {
+ level: 'ALERT',
+ error,
+ });
+
+ void logger.flush();
+ }
+
+ throw error;
+ }
if (parsedData.length > perPage) {
const nextItem = parsedData.pop();
diff --git a/packages/lib/utils/document-audit-logs.ts b/packages/lib/utils/document-audit-logs.ts
index 65ffb2817..3c5f01c97 100644
--- a/packages/lib/utils/document-audit-logs.ts
+++ b/packages/lib/utils/document-audit-logs.ts
@@ -9,6 +9,7 @@ import type {
} from '@documenso/prisma/client';
import { RECIPIENT_ROLES_DESCRIPTION } from '../constants/recipient-roles';
+import { AppError } from '../errors/app-error';
import type {
TDocumentAuditLog,
TDocumentAuditLogDocumentMetaDiffSchema,
@@ -69,7 +70,7 @@ export const parseDocumentAuditLogData = (auditLog: DocumentAuditLog): TDocument
// Handle any required migrations here.
if (!data.success) {
console.error(data.error);
- throw new Error('Migration required');
+ throw new AppError('MIGRATION_REQUIRED');
}
return data.data;
diff --git a/packages/lib/utils/logger.ts b/packages/lib/utils/logger.ts
new file mode 100644
index 000000000..ad46e6bb6
--- /dev/null
+++ b/packages/lib/utils/logger.ts
@@ -0,0 +1,16 @@
+import type { LoggerConfig } from 'next-axiom';
+import { Logger } from 'next-axiom';
+
+/**
+ * For usage in server-side code.
+ *
+ * When used in a server component, you must flush the logs.
+ *
+ * https://github.com/axiomhq/next-axiom?tab=readme-ov-file#server-components
+ */
+export const buildServerLogger = (config?: LoggerConfig) => {
+ return new Logger({
+ source: 'server',
+ ...config,
+ });
+};
diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts
index 70cf15291..02734f902 100644
--- a/packages/trpc/server/document-router/router.ts
+++ b/packages/trpc/server/document-router/router.ts
@@ -16,6 +16,7 @@ import { sendDocument } from '@documenso/lib/server-only/document/send-document'
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
+import { buildServerLogger } from '@documenso/lib/utils/logger';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
@@ -32,6 +33,12 @@ import {
ZSetTitleForDocumentMutationSchema,
} from './schema';
+const logger = buildServerLogger({
+ args: {
+ context: 'trpcDocumentRouter',
+ },
+});
+
export const documentRouter = router({
getDocumentById: authenticatedProcedure
.input(ZGetDocumentByIdQuerySchema)
@@ -42,6 +49,10 @@ export const documentRouter = router({
userId: ctx.user.id,
});
} catch (err) {
+ logger.error('getDocumentById', {
+ error: err,
+ });
+
console.error(err);
throw new TRPCError({
@@ -59,6 +70,10 @@ export const documentRouter = router({
token,
});
} catch (err) {
+ logger.error('getDocumentByToken', {
+ error: err,
+ });
+
console.error(err);
throw new TRPCError({
@@ -77,6 +92,10 @@ export const documentRouter = router({
userId: ctx.user.id,
});
} catch (err) {
+ logger.error('getDocumentWithDetailsById', {
+ error: err,
+ });
+
console.error(err);
throw new TRPCError({
@@ -110,6 +129,10 @@ export const documentRouter = router({
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
+ logger.error('createDocument', {
+ error: err,
+ });
+
if (err instanceof TRPCError) {
throw err;
}
@@ -136,6 +159,10 @@ export const documentRouter = router({
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
+ logger.error('deleteDocument', {
+ error: err,
+ });
+
console.error(err);
throw new TRPCError({
@@ -161,6 +188,10 @@ export const documentRouter = router({
userId: ctx.user.id,
});
} catch (err) {
+ logger.error('findDocumentAuditLogs', {
+ error: err,
+ });
+
console.error(err);
throw new TRPCError({
@@ -177,13 +208,21 @@ export const documentRouter = router({
const userId = ctx.user.id;
- return await updateTitle({
- title,
- userId,
- teamId,
- documentId,
- requestMetadata: extractNextApiRequestMetadata(ctx.req),
- });
+ try {
+ return await updateTitle({
+ title,
+ userId,
+ teamId,
+ documentId,
+ requestMetadata: extractNextApiRequestMetadata(ctx.req),
+ });
+ } catch (err) {
+ logger.error('setTitleForDocument', {
+ error: err,
+ });
+
+ console.error(err);
+ }
}),
setPasswordForDocument: authenticatedProcedure
@@ -210,6 +249,10 @@ export const documentRouter = router({
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
+ logger.error('setPasswordForDocument', {
+ error: err,
+ });
+
console.error(err);
throw new TRPCError({
@@ -245,6 +288,10 @@ export const documentRouter = router({
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
+ logger.error('sendDocument', {
+ error: err,
+ });
+
console.error(err);
throw new TRPCError({
@@ -264,6 +311,10 @@ export const documentRouter = router({
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
+ logger.error('resendDocument', {
+ error: err,
+ });
+
console.error(err);
throw new TRPCError({
@@ -282,6 +333,10 @@ export const documentRouter = router({
...input,
});
} catch (err) {
+ logger.error('duplicateDocument', {
+ error: err,
+ });
+
console.log(err);
throw new TRPCError({
@@ -302,7 +357,11 @@ export const documentRouter = router({
userId: ctx.user.id,
});
return documents;
- } catch (error) {
+ } catch (err) {
+ logger.error('searchDocuments', {
+ error: err,
+ });
+
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We are unable to search for documents. Please try again later.',