diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 4bc860f8..00000000
--- a/.eslintignore
+++ /dev/null
@@ -1,9 +0,0 @@
-build
-public
-coverage
-cypress/integration/examples
-node_modules
-.yarn/.cache
-.husky
-.nyc_output
-.yarn
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index c6ddc9f3..00000000
--- a/.eslintrc
+++ /dev/null
@@ -1,117 +0,0 @@
-{
- "extends": [
- "airbnb",
- "airbnb-typescript",
- "prettier",
- "plugin:prettier/recommended"
- ],
- "plugins": [
- "import",
- "jsx-a11y",
- "react",
- "prettier"
- ],
- "env": {
- "browser": true,
- "node": true,
- "mocha": true,
- "jest": true
- },
- "globals": {
- "cy": true,
- "Cypress": true,
- "JSX": true
- },
- "parser": "@typescript-eslint/parser",
- "parserOptions": {
- "project": "./tsconfig.json"
- },
- "rules": {
- "import/order": "off",
- "prettier/prettier": "error",
- "no-console": [
- "error",
- {
- "allow": [
- "warn",
- "error"
- ]
- }
- ],
- "react/function-component-definition": [
- 2,
- {
- "namedComponents": "arrow-function"
- }
- ],
- "curly": "error",
- "@typescript-eslint/indent": "off",
- "@typescript-eslint/brace-style": "off",
- "no-underscore-dangle": [
- "error",
- {
- "allow": [
- "_id",
- "__REDUX_DEVTOOLS_EXTENSION__"
- ]
- }
- ],
- "jsx-a11y/anchor-is-valid": [
- "error",
- {
- "components": [
- "Link"
- ],
- "specialLink": [
- "to",
- "hrefLeft",
- "hrefRight"
- ],
- "aspects": [
- "noHref",
- "invalidHref",
- "preferButton"
- ]
- }
- ],
- "react/jsx-filename-extension": [
- 1,
- {
- "extensions": [
- ".js",
- ".jsx",
- ".ts",
- ".tsx"
- ]
- }
- ],
- "react/react-in-jsx-scope": "off",
- "react/require-default-props": "off",
- "import/no-named-as-default": 0,
- "import/no-extraneous-dependencies": [
- "error",
- {
- "devDependencies": true
- }
- ],
- "react/static-property-placement": [
- "error",
- "property assignment",
- {
- "childContextTypes": "static public field",
- "contextTypes": "static public field",
- "contextType": "static public field",
- "defaultProps": "static public field",
- "displayName": "static public field",
- "propTypes": "static public field"
- }
- ],
- "react/state-in-constructor": [
- "error",
- "never"
- ]
- },
- "ignorePatterns": [
- "node_modules/*"
- ]
-}
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 00000000..bffb357a
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/.gitignore b/.gitignore
index f9b7562b..3b83b474 100644
--- a/.gitignore
+++ b/.gitignore
@@ -72,3 +72,6 @@ next-env.d.ts
# Sentry Auth Token
.sentryclirc
+
+# Sentry Config File
+.env.sentry-build-plugin
diff --git a/app/api/sentry-example-api/route.ts b/app/api/sentry-example-api/route.ts
new file mode 100644
index 00000000..abf82362
--- /dev/null
+++ b/app/api/sentry-example-api/route.ts
@@ -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...' });
+}
diff --git a/app/global-error.tsx b/app/global-error.tsx
new file mode 100644
index 00000000..c550741f
--- /dev/null
+++ b/app/global-error.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import * as Sentry from '@sentry/nextjs';
+import NextError from 'next/error';
+
+import { useEffect } from 'react';
+
+export default function GlobalError({
+ error,
+}: {
+ error: Error & { digest?: string };
+}) {
+ useEffect(() => {
+ Sentry.captureException(error);
+ }, [error]);
+
+ return (
+
+
+ {/* `NextError` is the default Next.js error page component. Its type
+ definition requires a `statusCode` prop. However, since the App Router
+ does not expose status codes for errors, we simply pass 0 to render a
+ generic error message. */}
+
+
+
+ );
+}
diff --git a/app/providers.tsx b/app/providers.tsx
index 3b8833fc..4943655e 100644
--- a/app/providers.tsx
+++ b/app/providers.tsx
@@ -26,29 +26,37 @@ export default function Providers(props: { children: React.ReactNode }) {
const { children } = props;
- useEffect(() => {
- // todo: check validation is correct
- if (
- pathname !== currentPathState.pathname ||
- searchParams !== currentPathState.searchParams
- ) {
- // REACTGA
- // Send pageview with a custom path
- if (GA_MEASUREMENT_ID && hasAcceptedCookies() && NODE_ENV !== ENV.TEST) {
- ReactGA.initialize(GA_MEASUREMENT_ID);
- ReactGA.send('pageview');
+ useEffect(
+ () => {
+ // todo: check validation is correct
+ if (
+ pathname !== currentPathState.pathname ||
+ searchParams !== currentPathState.searchParams
+ ) {
+ // REACTGA
+ // Send pageview with a custom path
+ if (
+ GA_MEASUREMENT_ID &&
+ hasAcceptedCookies() &&
+ NODE_ENV !== ENV.TEST
+ ) {
+ ReactGA.initialize(GA_MEASUREMENT_ID);
+ ReactGA.send('pageview');
+ }
+ setCurrentPathState({ pathname, searchParams });
+
+ // remove cross domain tracking query params
+ const params = new URLSearchParams(searchParams ?? {});
+ params.delete(UrlSearch.GACrossDomainKey);
+ // todo: check replace correctly
+ // add back shallow
+ // https://github.com/vercel/next.js/discussions/48110#discussioncomment-7563979
+ replace(`${pathname}?${params.toString()}`);
}
- setCurrentPathState({ pathname, searchParams });
-
- // remove cross domain tracking query params
- const params = new URLSearchParams(searchParams ?? {});
- params.delete(UrlSearch.GACrossDomainKey);
- // todo: check replace correctly
- // add back shallow
- // https://github.com/vercel/next.js/discussions/48110#discussioncomment-7563979
- replace(`${pathname}?${params.toString()}`);
- }
- }, [pathname, searchParams]);
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [pathname, searchParams],
+ );
return (
diff --git a/app/sentry-example-page/page.tsx b/app/sentry-example-page/page.tsx
new file mode 100644
index 00000000..7e7b2e76
--- /dev/null
+++ b/app/sentry-example-page/page.tsx
@@ -0,0 +1,85 @@
+'use client';
+
+import * as Sentry from '@sentry/nextjs';
+import Head from 'next/head';
+
+export default function Page() {
+ return (
+
+
+
Sentry Onboarding
+
+
+
+
+
+
+
+
+ Get started by sending us a sample error:
+
+
+
+ Next, look for the error on the{' '}
+
+ Issues Page
+
+ .
+
+
+ For more information, see{' '}
+
+ https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+
+
+
+ );
+}
diff --git a/cypress/e2e/collection/summary.cy.ts b/cypress/e2e/collection/summary.cy.ts
index c30dba63..21be59ce 100644
--- a/cypress/e2e/collection/summary.cy.ts
+++ b/cypress/e2e/collection/summary.cy.ts
@@ -158,8 +158,7 @@ describe('Collection Summary', () => {
cy.wait('@copy').then(({ request: { url, body } }) => {
expect(url).to.contain(item.id);
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
- expect(body.to).to.be.undefined;
+ expect(body.to).be.undefined;
});
// copy child item on home
@@ -172,7 +171,6 @@ describe('Collection Summary', () => {
cy.wait('@copy').then(({ request: { url, body } }) => {
expect(url).to.contain(child.id);
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(body.to).to.be.undefined;
});
});
diff --git a/cypress/e2e/home/search.cy.ts b/cypress/e2e/home/search.cy.ts
index dd3e8837..2154757d 100644
--- a/cypress/e2e/home/search.cy.ts
+++ b/cypress/e2e/home/search.cy.ts
@@ -56,7 +56,6 @@ describe('Search', () => {
cy.wait(['@search', '@search']).then(
([
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
_first,
{
request: { body },
diff --git a/instrumentation.ts b/instrumentation.ts
new file mode 100644
index 00000000..7b89a972
--- /dev/null
+++ b/instrumentation.ts
@@ -0,0 +1,9 @@
+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');
+ }
+}
diff --git a/next.config.js b/next.config.js
index 30f1c4c7..08f4055a 100644
--- a/next.config.js
+++ b/next.config.js
@@ -6,35 +6,36 @@ module.exports = {
const { withSentryConfig } = require('@sentry/nextjs');
-module.exports = withSentryConfig(
- module.exports,
- {
- // For all available options, see:
- // https://github.com/getsentry/sentry-webpack-plugin#options
+module.exports = withSentryConfig(module.exports, {
+ // For all available options, see:
+ // https://github.com/getsentry/sentry-webpack-plugin#options
- // Suppresses source map uploading logs during build
- silent: true,
+ org: 'graasp',
+ project: 'graasp-library',
- org: 'graasp',
- project: 'graasp-library',
- },
- {
- // For all available options, see:
- // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
+ // 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,
+ // Upload a larger set of source maps for prettier stack traces (increases build time)
+ widenClientFileUpload: true,
- // Transpiles SDK to be compatible with IE11 (increases bundle size)
- transpileClientSDK: true,
+ // Automatically annotate React components to show their full name in breadcrumbs and session replay
+ reactComponentAnnotation: {
+ enabled: true,
+ },
- // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
- tunnelRoute: '/monitoring',
+ // 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,
+ // Hides source maps from generated client bundles
+ hideSourceMaps: true,
- // Automatically tree-shake Sentry logger statements to reduce bundle size
- disableLogger: true,
- },
-);
+ // Automatically tree-shake Sentry logger statements to reduce bundle size
+ disableLogger: true,
+});
diff --git a/package.json b/package.json
index 27f9e0d0..b98969dc 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"hooks:uninstall": "husky uninstall",
"hooks:install": "husky install",
"test:once": "exit 0",
- "lint": "eslint .",
+ "lint": "next lint",
"prettier:check": "prettier --check '{app,src,pages,cypress}/**/*.{js,ts,tsx}'",
"prettier:write": "prettier --write '{app,src,pages,cypress}/**/*.{js,ts,tsx}'",
"check": "yarn lint && yarn prettier:check && yarn tsc --noEmit",
@@ -30,25 +30,25 @@
},
"dependencies": {
"@emotion/cache": "11.13.1",
- "@emotion/react": "11.13.0",
+ "@emotion/react": "11.13.3",
"@emotion/server": "11.11.0",
"@emotion/styled": "11.13.0",
"@graasp/query-client": "3.22.2",
"@graasp/sdk": "4.26.0",
- "@graasp/translations": "1.35.0",
+ "@graasp/translations": "1.35.1",
"@graasp/ui": "4.26.0",
- "@mui/icons-material": "5.16.5",
+ "@mui/icons-material": "5.16.7",
"@mui/lab": "5.0.0-alpha.170",
- "@mui/material": "5.16.5",
- "@sentry/nextjs": "8.26.0",
+ "@mui/material": "5.16.7",
+ "@sentry/nextjs": "^8",
"@testing-library/jest-dom": "6.4.8",
"@testing-library/react": "16.0.0",
"@testing-library/user-event": "14.5.2",
"@trivago/prettier-plugin-sort-imports": "4.3.0",
"date-fns": "3.6.0",
- "eslint-config-next": "14.2.5",
+ "eslint-config-next": "14.2.6",
"http-status-codes": "2.3.0",
- "i18next": "23.12.2",
+ "i18next": "23.14.0",
"interweave": "13.1.0",
"katex": "0.16.11",
"lodash.groupby": "4.6.0",
@@ -56,40 +56,38 @@
"lodash.isstring": "4.0.1",
"lodash.truncate": "4.4.2",
"lucide-react": "0.429.0",
- "next": "14.2.5",
+ "next": "14.2.6",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-ga4": "2.1.0",
"react-helmet": "6.1.0",
- "react-i18next": "15.0.0",
+ "react-i18next": "15.0.1",
"react-quill": "2.0.0",
"react-router-dom": "6.26.1",
"react-toastify": "10.0.5",
"social-links": "1.14.0",
- "stylis": "4.3.2",
+ "stylis": "4.3.3",
"stylis-plugin-rtl": "2.1.1",
"uuid": "10.0.0"
},
"devDependencies": {
"@commitlint/cli": "19.4.0",
"@commitlint/config-conventional": "19.2.2",
- "@cypress/code-coverage": "3.12.44",
+ "@cypress/code-coverage": "3.12.45",
"@types/katex": "^0.16.7",
"@types/lodash.groupby": "4.6.9",
"@types/lodash.truncate": "4.4.9",
- "@types/node": "20.14.13",
- "@types/react": "18.3.3",
+ "@types/node": "20.16.1",
+ "@types/react": "18.3.4",
"@types/react-dom": "18.3.0",
"@types/react-helmet": "6.1.11",
"@types/uuid": "10.0.0",
- "@typescript-eslint/eslint-plugin": "7.18.0",
- "@typescript-eslint/parser": "7.18.0",
+ "@typescript-eslint/eslint-plugin": "8.2.0",
+ "@typescript-eslint/parser": "8.2.0",
"concurrently": "8.2.2",
- "cypress": "13.13.1",
+ "cypress": "13.13.3",
"env-cmd": "10.1.0",
- "eslint": "8.57.0",
- "eslint-config-airbnb": "19.0.4",
- "eslint-config-airbnb-typescript": "18.0.0",
+ "eslint": "v8",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
@@ -97,13 +95,13 @@
"eslint-plugin-prettier": "5.2.1",
"eslint-plugin-react": "7.35.0",
"eslint-plugin-react-app": "6.2.2",
- "husky": "9.1.4",
+ "husky": "9.1.5",
"nyc": "17.0.0",
"prettier": "3.3.3",
"react-query": "3.39.3",
"standard-version": "9.5.0",
"typescript": "5.5.4",
- "wait-on": "7.2.0"
+ "wait-on": "8.0.0"
},
"nyc": {
"exclude": [
diff --git a/sentry.client.config.ts b/sentry.client.config.ts
index 0ad00be0..ce36b67d 100644
--- a/sentry.client.config.ts
+++ b/sentry.client.config.ts
@@ -13,10 +13,9 @@ Sentry.init({
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
-
replaysOnErrorSampleRate: 1.0,
- // This sets the sample rate to be 30%. You may want this to be 100% while
+ // 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.3,
diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts
index 1035f3be..97a2358b 100644
--- a/sentry.edge.config.ts
+++ b/sentry.edge.config.ts
@@ -11,6 +11,7 @@ Sentry.init({
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
+
release: SENTRY_RELEASE,
environment: SENTRY_ENV,
diff --git a/sentry.server.config.ts b/sentry.server.config.ts
index 82c0672e..e88a519d 100644
--- a/sentry.server.config.ts
+++ b/sentry.server.config.ts
@@ -10,9 +10,13 @@ Sentry.init({
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
- release: SENTRY_RELEASE,
- environment: SENTRY_ENV,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
+
+ release: SENTRY_RELEASE,
+ environment: SENTRY_ENV,
+
+ // Uncomment the line below to enable Spotlight (https://spotlightjs.com)
+ // spotlight: process.env.NODE_ENV === 'development',
});
diff --git a/src/components/collection/ChildrenCard.tsx b/src/components/collection/ChildrenCard.tsx
index 93cd1d38..68b4e2f7 100644
--- a/src/components/collection/ChildrenCard.tsx
+++ b/src/components/collection/ChildrenCard.tsx
@@ -186,6 +186,7 @@ export const FileChildrenCard: React.FC = ({
/>
),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[thumbnailUrl],
);
diff --git a/src/components/collection/Collection.tsx b/src/components/collection/Collection.tsx
index 62267513..351f99f8 100644
--- a/src/components/collection/Collection.tsx
+++ b/src/components/collection/Collection.tsx
@@ -42,11 +42,15 @@ const Collection = ({ id }: Props) => {
const { mutate: postView } = mutations.usePostItemAction();
- useEffect(() => {
- if (id) {
- postView({ itemId: id, payload: { type: 'collection-view' } });
- }
- }, [id]);
+ useEffect(
+ () => {
+ if (id) {
+ postView({ itemId: id, payload: { type: 'collection-view' } });
+ }
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [id],
+ );
// if tags could be fetched then user has at least read access
const canRead = Boolean(tags);
diff --git a/src/components/collection/CopyLinkButton.tsx b/src/components/collection/CopyLinkButton.tsx
index 60c61f66..5a8308d3 100644
--- a/src/components/collection/CopyLinkButton.tsx
+++ b/src/components/collection/CopyLinkButton.tsx
@@ -16,11 +16,12 @@ import { copyToClipboard } from '../../utils/clipboard';
import { QueryClientContext } from '../QueryClientContext';
export const useEmbedAction = (itemId?: DiscriminatedItem['id']) => {
+ const { mutations } = useContext(QueryClientContext);
+ const { mutate: triggerAction } = mutations.usePostItemAction();
+
const startEmbed = (event: MouseEvent) => {
const link = buildPlayerViewItemRoute(itemId);
- const { mutations } = useContext(QueryClientContext);
- const { mutate: triggerAction } = mutations.usePostItemAction();
copyToClipboard(link, {
onSuccess: () => {
if (itemId) {
@@ -49,6 +50,7 @@ export const useEmbedAction = (itemId?: DiscriminatedItem['id']) => {
startEmbed,
};
};
+
type CopyLinkButtonProps = { itemId: DiscriminatedItem['id'] };
const CopyLinkButton = ({ itemId }: CopyLinkButtonProps) => {
diff --git a/src/components/collection/ItemBreadcrumb.tsx b/src/components/collection/ItemBreadcrumb.tsx
index 3eec4b6d..8d1e0d6c 100644
--- a/src/components/collection/ItemBreadcrumb.tsx
+++ b/src/components/collection/ItemBreadcrumb.tsx
@@ -56,7 +56,11 @@ const ItemBreadcrumb = ({
return (
{parents?.map((parent) => (
-