Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/plausible #32

Closed
wants to merge 10 commits into from
6 changes: 5 additions & 1 deletion app/(dashboard)/dashboard/_components/ProtocolUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ChevronDown, ChevronUp } from 'lucide-react';
import type { FileWithPath } from 'react-dropzone';
import { generateReactHelpers } from '@uploadthing/react/hooks';
import { useState, useCallback } from 'react';

import { importProtocol } from '../_actions/importProtocol';
import { Button } from '~/components/ui/Button';

Expand All @@ -21,6 +20,7 @@ import type { UploadFileResponse } from 'uploadthing/client';
import { Collapsible, CollapsibleContent } from '~/components/ui/collapsible';
import ActiveProtocolSwitch from '~/app/(dashboard)/dashboard/_components/ActiveProtocolSwitch';
import { api } from '~/trpc/client';
import { useAnalytics } from '~/hooks/useAnalytics';

export default function ProtocolUploader({
onUploaded,
Expand All @@ -37,6 +37,8 @@ export default function ProtocolUploader({
});

const utils = api.useUtils();
const appSettings = api.appSettings.get.useQuery();
const trackEvent = useAnalytics();

const handleUploadComplete = async (
res: UploadFileResponse[] | undefined,
Expand Down Expand Up @@ -67,6 +69,8 @@ export default function ProtocolUploader({

await utils.protocol.get.lastUploaded.refetch();

trackEvent('ProtocolImported', appSettings?.data?.installationId);

setDialogContent({
title: 'Protocol import',
description: 'Protocol successfully imported!',
Expand Down
2 changes: 2 additions & 0 deletions app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ResetButton from './_components/ResetButton';
import AnonymousRecruitmentSwitch from '~/components/AnonymousRecruitmentSwitch/AnonymousRecruitmentSwitch';
import Link from 'next/link';
import { Button } from '~/components/ui/Button';
import AnalyticsSwitch from '~/components/AnalyticsSwitch/AnalyticsSwitch';

function Home() {
return (
Expand All @@ -14,6 +15,7 @@ function Home() {
</Link>
<ResetButton />
<AnonymousRecruitmentSwitch />
<AnalyticsSwitch />
</main>
</>
);
Expand Down
39 changes: 39 additions & 0 deletions app/(onboard)/_components/OnboardSteps/Analytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';
import { Button } from '~/components/ui/Button';
import { useOnboardingContext } from '../OnboardingProvider';
import AnalyticsSwitch from '~/components/AnalyticsSwitch/Switch';
import { api } from '~/trpc/client';

function Analytics() {
const { currentStep, setCurrentStep } = useOnboardingContext();

const handleNextStep = () => {
setCurrentStep(currentStep + 1).catch(() => {});
};

const appSettings = api.appSettings.get.useQuery(undefined, {
onError(error) {
throw new Error(error.message);
},
});
return (
<div className="max-w-[30rem]">
<div className="mb-4 flex flex-col">
<h1 className="text-3xl font-bold">Analytics</h1>
<p className="mb-4 mt-4">
By default, the app is configured to allow collection of analytics.
Analytics collection can be disabled here or in the app settings on
the dashboard.
</p>
</div>
<div>
<AnalyticsSwitch allowAnalytics={!!appSettings?.data?.allowAnalytics} />
<div className="flex justify-start">
<Button onClick={handleNextStep}>Next</Button>
</div>
</div>
</div>
);
}

export default Analytics;
13 changes: 12 additions & 1 deletion app/(onboard)/_components/OnboardSteps/Documentation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@ import { Card, CardContent, CardHeader, CardTitle } from '~/components/ui/card';
import { FileText, MonitorPlay } from 'lucide-react';
import { setAppConfigured } from '~/app/_actions';
import SubmitButton from '~/components/ui/SubmitButton';
import { api } from '~/trpc/client';
import { useAnalytics } from '~/hooks/useAnalytics';

function Documentation() {
const appSettings = api.appSettings.get.useQuery();
const trackEvent = useAnalytics();

const handleAppConfigured = async () => {
await setAppConfigured();

trackEvent('AppSetup', appSettings?.data?.installationId);
};

return (
<div className="max-w-[30rem]">
<div className="mb-4 flex flex-col">
Expand Down Expand Up @@ -44,7 +55,7 @@ function Documentation() {
</Card>

<div className="flex justify-start pt-12">
<form action={setAppConfigured}>
<form action={handleAppConfigured}>
<SubmitButton variant="default" size={'lg'}>
Go to the dashboard!
</SubmitButton>
Expand Down
1 change: 1 addition & 0 deletions app/(onboard)/_components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const stepLabels = [
'Create Account',
'Upload Protocol',
'Configure Participation',
'Analytics',
'Documentation',
];

Expand Down
11 changes: 11 additions & 0 deletions app/(onboard)/setup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ const ManageParticipants = dynamic(
loading: () => <StepLoadingState key="loading" />,
},
);
const Analytics = dynamic(
() => import('../_components/OnboardSteps/Analytics'),
{
loading: () => <StepLoadingState key="loading" />,
},
);
const Documentation = dynamic(
() => import('../_components/OnboardSteps/Documentation'),
{
Expand Down Expand Up @@ -104,6 +110,11 @@ function Page() {
</StepMotionWrapper>
)}
{currentStep === 4 && (
<StepMotionWrapper key="docs">
<Analytics />
</StepMotionWrapper>
)}
{currentStep === 5 && (
<StepMotionWrapper key="docs">
<Documentation />
</StepMotionWrapper>
Expand Down
11 changes: 11 additions & 0 deletions components/AnalyticsSwitch/AnalyticsSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { api } from '~/trpc/server';
import Switch from './Switch';
import 'server-only';

const AnalyticsSwitch = async () => {
const appSettings = await api.appSettings.get.query();

return <Switch allowAnalytics={!!appSettings?.allowAnalytics} />;
};

export default AnalyticsSwitch;
52 changes: 52 additions & 0 deletions components/AnalyticsSwitch/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client';

import { Switch as SwitchUI } from '~/components/ui/switch';
import { setAnalytics } from './action';
import { useOptimistic, useTransition } from 'react';
import { api } from '~/trpc/client';

const Switch = ({ allowAnalytics }: { allowAnalytics: boolean }) => {
const [, startTransition] = useTransition();
const [optimisticAllowAnalytics, setOptimisticAllowAnalytics] = useOptimistic(
allowAnalytics,
(state: boolean, newState: boolean) => newState,
);
const utils = api.useUtils();

return (
<div className="mb-4">
<div className="flex items-center justify-between">
<div className="mr-20">
<h3 className="font-bold">Allow Analytics</h3>
<p className="text-sm text-gray-600">
Information collected includes the number of protocols uploaded,
interviews conducted, participants recruited, and participants who
completed the interview. No personally identifiable information,
interview data, or protocol data is collected.
</p>
</div>
<SwitchUI
name="allowAnalytics"
checked={optimisticAllowAnalytics}
onCheckedChange={(value) => {
startTransition(async () => {
setOptimisticAllowAnalytics(value);

try {
await setAnalytics(value);
await utils.appSettings.get.refetch();
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
}
throw new Error('Something went wrong');
}
});
}}
/>
</div>
</div>
);
};

export default Switch;
7 changes: 7 additions & 0 deletions components/AnalyticsSwitch/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use server';

import { api } from '~/trpc/server';

export async function setAnalytics(state: boolean) {
await api.appSettings.updateAnalytics.mutate(state);
}
13 changes: 13 additions & 0 deletions hooks/useAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { usePlausible } from 'next-plausible';

export function useAnalytics() {
const plausible = usePlausible();

const trackEvent = (eventName: string, installationId?: string) => {
installationId
? plausible(eventName, { props: { installationId } })
: plausible(eventName);
};

return trackEvent;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"lucia": "^2.7.2",
"lucide-react": "^0.286.0",
"next": "^14.0.0",
"next-plausible": "^3.11.3",
"next-usequerystate": "^1.8.4",
"papaparse": "^5.4.1",
"react": "18.2.0",
Expand Down
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,6 @@ model AppSettings {
configured Boolean @default(false)
initializedAt DateTime @default(now())
allowAnonymousRecruitment Boolean @default(false)

@@id([configured, initializedAt])
installationId String @unique @default(cuid())
allowAnalytics Boolean @default(true)
}
20 changes: 18 additions & 2 deletions providers/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,34 @@ import { TRPCReactProvider } from '~/trpc/client';
import { SessionProvider } from '~/providers/SessionProvider';
import type { Session } from 'lucia';
import { headers } from 'next/headers';
import PlausibleProvider from 'next-plausible';
import { env } from 'process';
import { api } from '~/trpc/server';

export default function Providers({
export default async function Providers({
children,
initialSession,
}: {
children: React.ReactNode;
initialSession: Session | null;
}): ReactElement {
}): Promise<ReactElement> {
const appSettings = await api.appSettings.get.query();

return (
<TRPCReactProvider headers={headers()}>
<ReactQueryDevtools initialIsOpen={true} />
<SessionProvider session={initialSession}>{children}</SessionProvider>
<PlausibleProvider
domain="fresco.networkcanvas.com"
taggedEvents={true}
manualPageviews={true}
trackLocalhost={env.NODE_ENV === 'development' ? true : false}
enabled={appSettings?.allowAnalytics}
// for production, add env.NODE_ENV === 'production' to disable tracking in dev
//enabled={appSettings?.allowAnalytics && env.NODE_ENV === 'production'}
selfHosted={true}
customDomain="https://analytics.networkcanvas.dev"
/>
</TRPCReactProvider>
);
}
17 changes: 17 additions & 0 deletions server/routers/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ export const appSettingsRouter = router({

revalidateTag('appSettings.get');

return { error: null, appSettings: updatedappSettings };
} catch (error) {
return { error: 'Failed to update appSettings', appSettings: null };
}
}),
updateAnalytics: protectedProcedure
.input(z.boolean())
.mutation(async ({ input }) => {
try {
const updatedappSettings = await prisma.appSettings.updateMany({
data: {
allowAnalytics: input,
},
});

revalidateTag('appSettings.get');

return { error: null, appSettings: updatedappSettings };
} catch (error) {
return { error: 'Failed to update appSettings', appSettings: null };
Expand Down