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
//