diff --git a/.env.test b/.env.test index a10a496..8a3701a 100644 --- a/.env.test +++ b/.env.test @@ -4,3 +4,4 @@ GOTRUE_SECURITY_CAPTCHA_ENABLED="true" GOTRUE_SECURITY_CAPTCHA_PROVIDER="turnstile" GOTRUE_SECURITY_CAPTCHA_SECRET="1x0000000000000000000000000000000AA" NEXT_PUBLIC_GOTRUE_SECURITY_CAPTCHA_SITEKEY="1x00000000000000000000BB" +NEXT_PUBLIC_ANALYTICS_SITE_ID='faithdashboard-test' diff --git a/app/privacy-policy/page.tsx b/app/privacy-policy/page.tsx index 3f1d31c..6e3dc46 100644 --- a/app/privacy-policy/page.tsx +++ b/app/privacy-policy/page.tsx @@ -10,7 +10,7 @@ async function PrivacyPolicy() { Dashboard collects and handles your personal information.

-

This privacy policy was last updated on June 27th, 2022.

+

This privacy policy was last updated on August 3rd, 2024.

Secure Connection

@@ -58,8 +58,16 @@ async function PrivacyPolicy() {

Analytics

- As of Februrary 9th, 2024, Faith Dashboard no longer collects any - analytics. + Faith Dashboard uses GoatCounter, + a privacy-focused analytics platform. We only use this information to + examine traffic trends and aggregated visitor statistics (i.e. the + percentage of Chrome users, or the total number of people who viewed a + particular page). However, you would need to read the{' '} + + GoatCounter privacy policy + + to understand what information GoatCounter collects. We do not otherwise + share this information.

Email Privacy

diff --git a/components/app/App.tsx b/components/app/App.tsx index 34aa793..ad72870 100644 --- a/components/app/App.tsx +++ b/components/app/App.tsx @@ -24,6 +24,7 @@ import ThemeMetadata from './ThemeMetadata'; import UpdateNotification from './UpdateNotification'; import { getDefaultAppState } from './appUtils'; import getAppNotificationMessage from './getAppNotificationMessage'; +import useAnalytics from './useAnalytics'; import useAppSync from './useAppSync'; import useThemeForEntirePage from './useThemeForEntirePage'; @@ -104,6 +105,7 @@ function App({ }, [setIsTutorialStarted]); useTouchDeviceDetection(); + useAnalytics(); const isMounted = useMountListener(); const isSignedIn = Boolean(user) && isSessionActive(session); diff --git a/components/app/useAnalytics.ts b/components/app/useAnalytics.ts new file mode 100644 index 0000000..4f48399 --- /dev/null +++ b/components/app/useAnalytics.ts @@ -0,0 +1,57 @@ +import { useEffect } from 'react'; + +// Define a basic type for the goatcounter global +declare global { + interface Window { + goatcounter?: { + count: (options: object) => void; + }; + } +} + +// Resolve a promise when GoatCounter is fully loaded and ready to use on the +// page +export async function getGoatcounter(): Promise< + NonNullable +> { + return new Promise((resolve) => { + if (window.goatcounter) { + resolve(window.goatcounter); + } else { + const script = document.createElement('script'); + script.addEventListener('load', () => { + if (window.goatcounter) { + resolve(window.goatcounter); + } else { + console.log('goatcounter script loaded but global not available'); + } + }); + script.async = true; + script.dataset.goatcounter = `https://${process.env.NEXT_PUBLIC_ANALYTICS_SITE_ID}.goatcounter.com/count`; + script.dataset.goatcounterSettings = JSON.stringify({ no_onload: true }); + script.src = 'https://gc.zgo.at/count.v4.js'; + script.crossOrigin = 'anonymous'; + script.integrity = + 'sha384-nRw6qfbWyJha9LhsOtSb2YJDyZdKvvCFh0fJYlkquSFjUxp9FVNugbfy8q1jdxI+'; + document.head.appendChild(script); + } + }); +} + +// Count a single pageview with GoatCounter +export async function countPageview() { + const goatcounter = await getGoatcounter(); + goatcounter.count({ + path: location.pathname + location.search + location.hash + }); +} + +// Use the +function useAnalytics() { + useEffect(() => { + if (process.env.NEXT_PUBLIC_ANALYTICS_SITE_ID) { + countPageview(); + } + }, []); +} +export default useAnalytics; diff --git a/middleware.ts b/middleware.ts index a8a18ac..040178f 100644 --- a/middleware.ts +++ b/middleware.ts @@ -11,7 +11,18 @@ import type { NextRequest } from 'next/server'; // ) function generateCSP() { const nonce = crypto.randomUUID(); - return `default-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://hcaptcha.com https://*.hcaptcha.com; font-src 'self' https://fonts.gstatic.com data:; img-src * data:; script-src 'self' 'nonce-${nonce}' https://storage.googleapis.com https://challenges.cloudflare.com; frame-src 'self' https://challenges.cloudflare.com; child-src 'self' https://challenges.cloudflare.com; connect-src *; manifest-src 'self'; media-src *;`; + return [ + `default-src 'none';`, + ` style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://hcaptcha.com https://*.hcaptcha.com;`, + ` font-src 'self' https://fonts.gstatic.com data:;`, + ` img-src * data:;`, + ` script-src 'self' 'nonce-${nonce}' https://storage.googleapis.com ${process.env.NEXT_PUBLIC_ANALYTICS_SITE_ID ? 'https://gc.zgo.at' : ''} https://challenges.cloudflare.com;`, + ` frame-src 'self' https://challenges.cloudflare.com;`, + ` child-src 'self' https://challenges.cloudflare.com;`, + ` connect-src *;`, + ` manifest-src 'self';`, + ` media-src *;` + ].join(' '); } // Source: diff --git a/next.config.js b/next.config.js index 40923f6..a8d76b1 100644 --- a/next.config.js +++ b/next.config.js @@ -21,9 +21,16 @@ const withPWA = require('next-pwa')({ // 'SKIP_WAITING'}." (source: // https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin.GenerateSW#GenerateSW) skipWaiting: false, - // Fix bad-precaching-response errors from service worker due to use of - // middleware (source: https://github.com/shadowwalker/next-pwa/issues/291) - runtimeCaching, + runtimeCaching: [ + // Fix bad-precaching-response errors from service worker due to use of + // middleware (source: https://github.com/shadowwalker/next-pwa/issues/291) + ...runtimeCaching, + // Fix no-response errors for GoatCounter analytics scripts + { + urlPattern: /^https:\/\/gc\.zgo\.at\//i, + handler: 'NetworkOnly' + } + ], buildExcludes: [ // This is necessary to prevent service worker errors; see //