Merge pull request #2 from MarconLP/feature/posthog-event-tracking
Feature/posthog event tracking
This commit is contained in:
commit
8ae1fce9d8
20 changed files with 559 additions and 148 deletions
54
package-lock.json
generated
54
package-lock.json
generated
|
|
@ -34,8 +34,9 @@
|
|||
"micro": "^10.0.1",
|
||||
"micro-cors": "^0.1.1",
|
||||
"next": "^13.3.0",
|
||||
"next-auth": "^4.21.0",
|
||||
"next-auth": "^4.22.1",
|
||||
"posthog-js": "^1.53.4",
|
||||
"posthog-node": "^3.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-media-recorder": "^1.6.6",
|
||||
|
|
@ -5349,9 +5350,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/next-auth": {
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.22.0.tgz",
|
||||
"integrity": "sha512-08+kjnDoE7aQ52O996x6cwA3ffc2CbHIkrCgLYhbE+aDIJBKI0oA9UbIEIe19/+ODYJgpAHHOtJx4izmsgaVag==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.22.1.tgz",
|
||||
"integrity": "sha512-NTR3f6W7/AWXKw8GSsgSyQcDW6jkslZLH8AiZa5PQ09w1kR8uHtR9rez/E9gAq/o17+p0JYHE8QjF3RoniiObA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@panva/hkdf": "^1.0.2",
|
||||
|
|
@ -5858,6 +5859,26 @@
|
|||
"rrweb-snapshot": "^1.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-node": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-3.1.0.tgz",
|
||||
"integrity": "sha512-hXhkDWigzNYgkfLpd3HbfCD0h1/zP19pPXEba2Daf6xCerrFxc7ixMDtXwCQMXOmJNvrcoVMvrjULpfKHN11vA==",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-node/node_modules/axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.13.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.13.2.tgz",
|
||||
|
|
@ -11126,9 +11147,9 @@
|
|||
}
|
||||
},
|
||||
"next-auth": {
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.22.0.tgz",
|
||||
"integrity": "sha512-08+kjnDoE7aQ52O996x6cwA3ffc2CbHIkrCgLYhbE+aDIJBKI0oA9UbIEIe19/+ODYJgpAHHOtJx4izmsgaVag==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.22.1.tgz",
|
||||
"integrity": "sha512-NTR3f6W7/AWXKw8GSsgSyQcDW6jkslZLH8AiZa5PQ09w1kR8uHtR9rez/E9gAq/o17+p0JYHE8QjF3RoniiObA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@panva/hkdf": "^1.0.2",
|
||||
|
|
@ -11451,6 +11472,25 @@
|
|||
"rrweb-snapshot": "^1.1.14"
|
||||
}
|
||||
},
|
||||
"posthog-node": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-3.1.0.tgz",
|
||||
"integrity": "sha512-hXhkDWigzNYgkfLpd3HbfCD0h1/zP19pPXEba2Daf6xCerrFxc7ixMDtXwCQMXOmJNvrcoVMvrjULpfKHN11vA==",
|
||||
"requires": {
|
||||
"axios": "^0.27.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"preact": {
|
||||
"version": "10.13.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.13.2.tgz",
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@
|
|||
"micro": "^10.0.1",
|
||||
"micro-cors": "^0.1.1",
|
||||
"next": "^13.3.0",
|
||||
"next-auth": "^4.21.0",
|
||||
"next-auth": "^4.22.1",
|
||||
"posthog-js": "^1.53.4",
|
||||
"posthog-node": "^3.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-media-recorder": "^1.6.6",
|
||||
|
|
|
|||
|
|
@ -5,22 +5,36 @@ import { useAtom } from "jotai";
|
|||
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
||||
import paywallAtom from "~/atoms/paywallAtom";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export default function NewVideoMenu() {
|
||||
const [, setRecordOpen] = useAtom(recordVideoModalOpen);
|
||||
const [, setUploadOpen] = useAtom(uploadVideoModalOpen);
|
||||
const [, setPaywallOpen] = useAtom(paywallAtom);
|
||||
const { data: session } = useSession();
|
||||
const posthog = usePostHog();
|
||||
|
||||
const openRecordModal = () => {
|
||||
setRecordOpen(true);
|
||||
|
||||
posthog?.capture("open record video modal", {
|
||||
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||
});
|
||||
};
|
||||
|
||||
const openUploadModal = () => {
|
||||
if (session?.user.stripeSubscriptionStatus === "active") {
|
||||
setUploadOpen(true);
|
||||
|
||||
posthog?.capture("open upload video modal", {
|
||||
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||
});
|
||||
} else {
|
||||
setPaywallOpen(true);
|
||||
|
||||
posthog?.capture("hit video upload paywall", {
|
||||
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { api } from "~/utils/api";
|
|||
import { useRouter } from "next/router";
|
||||
import { CheckIcon } from "@heroicons/react/20/solid";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export default function Paywall() {
|
||||
const { mutateAsync: createCheckoutSession } =
|
||||
|
|
@ -14,9 +15,12 @@ export default function Paywall() {
|
|||
const router = useRouter();
|
||||
const [open, setOpen] = useAtom(paywallAtom);
|
||||
const [billedAnnually, setBilledAnnually] = useState<boolean>(false);
|
||||
const posthog = usePostHog();
|
||||
|
||||
function closeModal() {
|
||||
setOpen(false);
|
||||
|
||||
posthog?.capture("close paywall");
|
||||
}
|
||||
|
||||
const handleCheckout = async () => {
|
||||
|
|
@ -26,6 +30,12 @@ export default function Paywall() {
|
|||
}
|
||||
};
|
||||
|
||||
const toggleBillingCycle = () => {
|
||||
setBilledAnnually(!billedAnnually);
|
||||
|
||||
posthog?.capture("change billing cycle");
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition appear show={open} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
|
|
@ -85,7 +95,7 @@ export default function Paywall() {
|
|||
<span className=" svelte-10wstod">Monthly</span>
|
||||
<label className="group relative flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600">
|
||||
<input
|
||||
onChange={() => setBilledAnnually(!billedAnnually)}
|
||||
onChange={toggleBillingCycle}
|
||||
checked={billedAnnually}
|
||||
type="checkbox"
|
||||
className="peer absolute left-1/2 hidden h-full w-full -translate-x-1/2 rounded-md"
|
||||
|
|
|
|||
|
|
@ -3,12 +3,26 @@ import { Fragment } from "react";
|
|||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { api } from "~/utils/api";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export default function ProfileMenu() {
|
||||
const { mutateAsync: createBillingPortalSession } =
|
||||
api.stripe.createBillingPortalSession.useMutation();
|
||||
const { push } = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const posthog = usePostHog();
|
||||
|
||||
const openBillingSettings = () => {
|
||||
void createBillingPortalSession().then(({ billingPortalUrl }) => {
|
||||
if (billingPortalUrl) {
|
||||
void push(billingPortalUrl);
|
||||
}
|
||||
});
|
||||
|
||||
posthog?.capture("billing settings opened", {
|
||||
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
|
|
@ -34,15 +48,7 @@ export default function ProfileMenu() {
|
|||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
void createBillingPortalSession().then(
|
||||
({ billingPortalUrl }) => {
|
||||
if (billingPortalUrl) {
|
||||
void push(billingPortalUrl);
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
onClick={openBillingSettings}
|
||||
className={`mx-2 flex h-8 w-40 cursor-pointer flex-row content-center rounded-md p-2 ${
|
||||
active ? "bg-gray-100" : ""
|
||||
}`}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { TRPCClientError } from "@trpc/client";
|
|||
import { useAtom } from "jotai/index";
|
||||
import paywallAtom from "~/atoms/paywallAtom";
|
||||
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
interface Props {
|
||||
closeModal: () => void;
|
||||
|
|
@ -45,6 +46,7 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
|||
const [duration, setDuration] = useState<number>(0);
|
||||
const [, setPaywallOpen] = useAtom(paywallAtom);
|
||||
const videoRef = useRef<null | HTMLVideoElement>(null);
|
||||
const posthog = usePostHog();
|
||||
|
||||
const handleRecording = async () => {
|
||||
const screenStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
|
|
@ -80,6 +82,8 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
|||
recorderRef.current.startRecording();
|
||||
|
||||
setStep("in");
|
||||
|
||||
posthog?.capture("recorder: start video recording");
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
|
|
@ -98,6 +102,8 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
|||
});
|
||||
|
||||
setStep("post");
|
||||
|
||||
posthog?.capture("recorder: video recording finished");
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
|
|
@ -109,6 +115,8 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
|||
|
||||
closeModal();
|
||||
setStep("pre");
|
||||
|
||||
posthog?.capture("recorder: video deleted");
|
||||
};
|
||||
|
||||
const handlePause = () => {
|
||||
|
|
@ -119,6 +127,7 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
|||
} else {
|
||||
recorderRef.current.pauseRecording();
|
||||
}
|
||||
posthog?.capture("recorder: recording paused/resumed", { pause });
|
||||
setPause(!pause);
|
||||
}
|
||||
};
|
||||
|
|
@ -146,6 +155,8 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
|||
"Recording - " + dayjs().format("D MMM YYYY") + ".webm";
|
||||
invokeSaveAsDialog(blob, dateString);
|
||||
}
|
||||
|
||||
posthog?.capture("recorder: video downloaded");
|
||||
};
|
||||
|
||||
const generateThumbnail = async (video: HTMLVideoElement) => {
|
||||
|
|
@ -187,6 +198,7 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
|||
.then(() => {
|
||||
void router.push("share/" + id);
|
||||
setRecordOpen(false);
|
||||
posthog?.capture("recorder: video uploaded");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
|
@ -197,6 +209,7 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
|||
err.message ===
|
||||
"Sorry, you have reached the maximum video upload limit on our free tier. Please upgrade to upload more."
|
||||
) {
|
||||
posthog?.capture("recorder: video upload paywall hit");
|
||||
setPaywallOpen(true);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -325,6 +338,8 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
|||
<video
|
||||
src={URL.createObjectURL(blob)}
|
||||
controls
|
||||
onPlay={() => posthog?.capture("recorder: played preview video")}
|
||||
onPause={() => posthog?.capture("recorder: paused preview video")}
|
||||
ref={videoRef}
|
||||
className="mb-4 w-[75vw]"
|
||||
/>
|
||||
|
|
@ -374,7 +389,10 @@ export default function Recorder({ closeModal, step, setStep }: Props) {
|
|||
<button
|
||||
type="button"
|
||||
className="ml-auto inline-flex items-center rounded-md bg-indigo-500 px-4 py-2 text-sm font-semibold leading-6 text-white shadow transition duration-150 ease-in-out hover:bg-indigo-400 disabled:cursor-not-allowed"
|
||||
onClick={() => void closeModal()}
|
||||
onClick={() => {
|
||||
posthog?.capture("recorder: closed post-modal");
|
||||
void closeModal();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Fragment, useState } from "react";
|
|||
import { ModernSwitch } from "~/components/ModernSwitch";
|
||||
import { api, type RouterOutputs } from "~/utils/api";
|
||||
import ExpireDateSelectMenu from "~/components/ExpireDateSelectMenu";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
interface Props {
|
||||
video: RouterOutputs["video"]["get"];
|
||||
|
|
@ -11,6 +12,23 @@ interface Props {
|
|||
export function ShareModal({ video }: Props) {
|
||||
const utils = api.useContext();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const posthog = usePostHog();
|
||||
|
||||
const openModal = () => {
|
||||
setOpen(true);
|
||||
posthog?.capture("open video share modal", {
|
||||
videoSharing: video.sharing,
|
||||
videoId: video.id,
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setOpen(false);
|
||||
posthog?.capture("close video share modal", {
|
||||
videoSharing: video.sharing,
|
||||
videoId: video.id,
|
||||
});
|
||||
};
|
||||
|
||||
const setSharingMutation = api.video.setSharing.useMutation({
|
||||
onMutate: async ({ videoId, sharing }) => {
|
||||
|
|
@ -58,23 +76,24 @@ export function ShareModal({ video }: Props) {
|
|||
setTimeout(() => {
|
||||
setLinkCopied(false);
|
||||
}, 5000);
|
||||
|
||||
posthog?.capture("public video link copied", {
|
||||
videoSharing: video.sharing,
|
||||
videoId: video.id,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
onClick={() => setOpen(true)}
|
||||
onClick={openModal}
|
||||
className="ml-4 cursor-pointer rounded border border-[#0000001a] px-2 py-2 text-sm text-[#292d34] hover:bg-[#fafbfc]"
|
||||
>
|
||||
Share
|
||||
</span>
|
||||
|
||||
<Transition appear show={open} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
|
|
@ -146,19 +165,6 @@ export function ShareModal({ video }: Props) {
|
|||
}
|
||||
/>
|
||||
</div>
|
||||
{/*<div className="mt-3 flex h-6 items-center justify-between">*/}
|
||||
{/* <span>Share link with search engines</span>*/}
|
||||
{/* <ModernSwitch*/}
|
||||
{/* enabled={video.linkShareSeo}*/}
|
||||
{/* toggle={() => console.log("test")}*/}
|
||||
{/* />*/}
|
||||
{/*</div>*/}
|
||||
{/*<div className="mt-3 flex h-6 items-center justify-between">*/}
|
||||
{/* <span>Embed code</span>*/}
|
||||
{/* <button className="h-6 rounded border border-solid border-[#d5d9df] bg-white px-[7px] font-medium">*/}
|
||||
{/* Copy code*/}
|
||||
{/* </button>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
TrashIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
interface Props {
|
||||
video: RouterOutputs["video"]["get"];
|
||||
|
|
@ -18,6 +19,7 @@ export default function VideoMoreMenu({ video }: Props) {
|
|||
const [renameMenuOpen, setRenameMenuOpen] = useState<boolean>(false);
|
||||
const utils = api.useContext();
|
||||
const router = useRouter();
|
||||
const posthog = usePostHog();
|
||||
|
||||
const items = [
|
||||
{
|
||||
|
|
@ -42,7 +44,10 @@ export default function VideoMoreMenu({ video }: Props) {
|
|||
a.download = video.title;
|
||||
a.click();
|
||||
});
|
||||
//window.location.href = response.url;
|
||||
});
|
||||
|
||||
posthog?.capture("download existing video", {
|
||||
videoId: video.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ const Recorder = dynamic(() => import("~/components/Recorder"), { ssr: false });
|
|||
import dynamic from "next/dynamic";
|
||||
import { useAtom } from "jotai";
|
||||
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export default function VideoRecordModal() {
|
||||
const [open, setOpen] = useAtom(recordVideoModalOpen);
|
||||
const [step, setStep] = useState<"pre" | "in" | "post">("pre");
|
||||
const posthog = usePostHog();
|
||||
|
||||
function closeModal() {
|
||||
setOpen(false);
|
||||
|
|
@ -15,6 +17,8 @@ export default function VideoRecordModal() {
|
|||
|
||||
const handleClose = () => {
|
||||
if (step === "pre") closeModal();
|
||||
|
||||
posthog?.capture("cancel video pre-recording");
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import axios from "axios";
|
|||
import { useRouter } from "next/router";
|
||||
import { useAtom } from "jotai";
|
||||
import uploadVideoModalOpen from "~/atoms/uploadVideoModalOpen";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export default function VideoUploadModal() {
|
||||
const [open, setOpen] = useAtom(uploadVideoModalOpen);
|
||||
|
|
@ -14,6 +16,8 @@ export default function VideoUploadModal() {
|
|||
const getSignedUrl = api.video.getUploadUrl.useMutation();
|
||||
const apiUtils = api.useContext();
|
||||
const videoRef = useRef<null | HTMLVideoElement>(null);
|
||||
const posthog = usePostHog();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
if (e.target.files) {
|
||||
|
|
@ -33,6 +37,10 @@ export default function VideoUploadModal() {
|
|||
|
||||
function closeModal() {
|
||||
setOpen(false);
|
||||
|
||||
posthog?.capture("cancel video upload", {
|
||||
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||
});
|
||||
}
|
||||
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { type AppType } from "next/app";
|
||||
import { type Session } from "next-auth";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { SessionProvider, useSession } from "next-auth/react";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
import "~/styles/globals.css";
|
||||
import CrispChat from "~/components/CrispChat";
|
||||
import posthog from "posthog-js";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
import { PostHogProvider, usePostHog } from "posthog-js/react";
|
||||
import { env } from "~/env.mjs";
|
||||
import { type ReactNode, useEffect } from "react";
|
||||
|
||||
// Check that PostHog is client-side (used to handle Next.js SSR)
|
||||
if (typeof window !== "undefined") {
|
||||
|
|
@ -28,11 +29,38 @@ const MyApp: AppType<{ session: Session | null }> = ({
|
|||
return (
|
||||
<SessionProvider session={session}>
|
||||
<PostHogProvider client={posthog}>
|
||||
<Component {...pageProps} />
|
||||
<PostHogIdentificationWrapper>
|
||||
<Component {...pageProps} />
|
||||
</PostHogIdentificationWrapper>
|
||||
<CrispChat />
|
||||
</PostHogProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const PostHogIdentificationWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const { data: session, status } = useSession();
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
if (!posthog) return;
|
||||
if (status === "authenticated") {
|
||||
const { id, name, email, stripeSubscriptionStatus } = session?.user;
|
||||
posthog.identify(id, {
|
||||
name,
|
||||
email,
|
||||
stripeSubscriptionStatus,
|
||||
});
|
||||
} else if (status === "unauthenticated") {
|
||||
posthog.reset();
|
||||
}
|
||||
}, [posthog, session, status]);
|
||||
|
||||
return <div>{children}</div>;
|
||||
};
|
||||
|
||||
export default api.withTRPC(MyApp);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
handleSubscriptionCreatedOrUpdated,
|
||||
} from "~/server/stripe-webhook-handlers";
|
||||
import { stripe } from "~/server/stripe";
|
||||
import { posthog } from "~/server/posthog";
|
||||
|
||||
// Stripe requires the raw body to construct the event.
|
||||
export const config = {
|
||||
|
|
@ -41,6 +42,7 @@ export default async function handler(
|
|||
event,
|
||||
stripe,
|
||||
prisma,
|
||||
posthog,
|
||||
});
|
||||
break;
|
||||
case "customer.subscription.created":
|
||||
|
|
@ -48,6 +50,7 @@ export default async function handler(
|
|||
await handleSubscriptionCreatedOrUpdated({
|
||||
event,
|
||||
prisma,
|
||||
posthog,
|
||||
});
|
||||
break;
|
||||
case "customer.subscription.updated":
|
||||
|
|
@ -55,6 +58,7 @@ export default async function handler(
|
|||
await handleSubscriptionCreatedOrUpdated({
|
||||
event,
|
||||
prisma,
|
||||
posthog,
|
||||
});
|
||||
break;
|
||||
case "invoice.payment_failed":
|
||||
|
|
@ -63,6 +67,17 @@ export default async function handler(
|
|||
// Use this webhook to notify your user that their payment has
|
||||
// failed and to retrieve new card details.
|
||||
// Can also have Stripe send an email to the customer notifying them of the failure. See settings: https://dashboard.stripe.com/settings/billing/automatic
|
||||
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const userId = subscription.metadata.userId;
|
||||
|
||||
if (userId) {
|
||||
posthog.capture({
|
||||
distinctId: userId,
|
||||
event: "stripe invoice.payment_failed",
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
}
|
||||
break;
|
||||
case "customer.subscription.deleted":
|
||||
// handle subscription cancelled automatically based
|
||||
|
|
@ -70,6 +85,7 @@ export default async function handler(
|
|||
await handleSubscriptionCanceled({
|
||||
event,
|
||||
prisma,
|
||||
posthog,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -11,17 +11,30 @@ import { ShareModal } from "~/components/ShareModal";
|
|||
import { useSession } from "next-auth/react";
|
||||
import VideoMoreMenu from "~/components/VideoMoreMenu";
|
||||
import ProfileMenu from "~/components/ProfileMenu";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
const VideoList: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { status, data: session } = useSession();
|
||||
const { videoId } = router.query as { videoId: string };
|
||||
const posthog = usePostHog();
|
||||
|
||||
const { data: video, isLoading } = api.video.get.useQuery(
|
||||
{ videoId },
|
||||
{
|
||||
enabled: router.isReady,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: (failureCount, error) => {
|
||||
if (error?.data?.code === "FORBIDDEN") return false;
|
||||
else return failureCount < 2;
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err?.data?.code === "FORBIDDEN") {
|
||||
posthog?.capture("video page: FORBIDDEN");
|
||||
} else if (err?.data?.code === "NOT_FOUND") {
|
||||
posthog?.capture("video page: NOT_FOUND");
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -33,7 +46,13 @@ const VideoList: NextPage = () => {
|
|||
</span>
|
||||
<span className="mt-3 max-w-[80%] text-center text-sm">
|
||||
To create your own public recordings,{" "}
|
||||
<Link href="/sign-in" className="pointer text-[#4169e1] underline">
|
||||
<Link
|
||||
onClick={() =>
|
||||
posthog?.capture("click sign-up from video error page")
|
||||
}
|
||||
href="/sign-in"
|
||||
className="pointer text-[#4169e1] underline"
|
||||
>
|
||||
create an account
|
||||
</Link>{" "}
|
||||
for free!
|
||||
|
|
@ -78,6 +97,30 @@ const VideoList: NextPage = () => {
|
|||
width="100%"
|
||||
height="100%"
|
||||
controls={true}
|
||||
onPlay={() =>
|
||||
posthog?.capture("play video", {
|
||||
videoId: video.id,
|
||||
videoCreatedAt: video.createdAt,
|
||||
videoUpdatedAt: video.updatedAt,
|
||||
videoUser: video.user.id,
|
||||
videoSharing: video.sharing,
|
||||
videoDeleteAfterLinkExpires:
|
||||
video.delete_after_link_expires,
|
||||
videoShareLinkExpiresAt: video.shareLinkExpiresAt,
|
||||
})
|
||||
}
|
||||
onPause={() =>
|
||||
posthog?.capture("pause video", {
|
||||
videoId: video.id,
|
||||
videoCreatedAt: video.createdAt,
|
||||
videoUpdatedAt: video.updatedAt,
|
||||
videoUser: video.user.id,
|
||||
videoSharing: video.sharing,
|
||||
videoDeleteAfterLinkExpires:
|
||||
video.delete_after_link_expires,
|
||||
videoShareLinkExpiresAt: video.shareLinkExpiresAt,
|
||||
})
|
||||
}
|
||||
url={video.video_url}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import uploadVideoModalOpen from "~/atoms/uploadVideoModalOpen";
|
|||
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
||||
import Paywall from "~/components/Paywall";
|
||||
import paywallAtom from "~/atoms/paywallAtom";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
const VideoList: NextPage = () => {
|
||||
const [, setRecordOpen] = useAtom(recordVideoModalOpen);
|
||||
|
|
@ -24,6 +25,7 @@ const VideoList: NextPage = () => {
|
|||
const router = useRouter();
|
||||
const { status, data: session } = useSession();
|
||||
const { data: videos, isLoading } = api.video.getAll.useQuery();
|
||||
const posthog = usePostHog();
|
||||
|
||||
if (status === "unauthenticated") {
|
||||
void router.replace("/sign-in");
|
||||
|
|
@ -31,13 +33,28 @@ const VideoList: NextPage = () => {
|
|||
|
||||
const openRecordModal = () => {
|
||||
setRecordOpen(true);
|
||||
|
||||
posthog?.capture("open record video modal", {
|
||||
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||
cta: "empty video list page",
|
||||
});
|
||||
};
|
||||
|
||||
const openUploadModal = () => {
|
||||
if (session?.user.stripeSubscriptionStatus === "active") {
|
||||
setUploadOpen(true);
|
||||
|
||||
posthog?.capture("open upload video modal", {
|
||||
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||
cta: "empty video list page",
|
||||
});
|
||||
} else {
|
||||
setPaywallOpen(true);
|
||||
|
||||
posthog?.capture("hit video upload paywall", {
|
||||
stripeSubscriptionStatus: session?.user.stripeSubscriptionStatus,
|
||||
cta: "empty video list page",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,64 @@ import { z } from "zod";
|
|||
export const stripeRouter = createTRPCRouter({
|
||||
createCheckoutSession: protectedProcedure
|
||||
.input(z.object({ billedAnnually: z.boolean() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { stripe, session, prisma, req } = ctx;
|
||||
.mutation(
|
||||
async ({ ctx: { prisma, stripe, session, req, posthog }, input }) => {
|
||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||
prisma,
|
||||
stripe,
|
||||
userId: session.user?.id,
|
||||
});
|
||||
|
||||
if (!customerId) {
|
||||
throw new Error("Could not create customer");
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
env.NODE_ENV === "development"
|
||||
? `http://${req.headers.host ?? "localhost:3000"}`
|
||||
: `https://${req.headers.host ?? env.NEXTAUTH_URL}`;
|
||||
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
client_reference_id: session.user?.id,
|
||||
payment_method_types: ["card"],
|
||||
allow_promotion_codes: true,
|
||||
mode: "subscription",
|
||||
line_items: [
|
||||
{
|
||||
price: input.billedAnnually
|
||||
? env.STRIPE_ANNUAL_PRICE_ID
|
||||
: env.STRIPE_MONTHLY_PRICE_ID,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${baseUrl}/videos?checkoutSuccess=true`,
|
||||
cancel_url: `${baseUrl}/videos?checkoutCanceled=true`,
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
userId: session.user?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!checkoutSession) {
|
||||
throw new Error("Could not create checkout session");
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "visiting checkout page",
|
||||
properties: {
|
||||
billingCycle: input.billedAnnually ? "annual" : "monthly",
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
|
||||
return { checkoutUrl: checkoutSession.url };
|
||||
}
|
||||
),
|
||||
createBillingPortalSession: protectedProcedure.mutation(
|
||||
async ({ ctx: { stripe, session, prisma, req, posthog } }) => {
|
||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||
prisma,
|
||||
stripe,
|
||||
|
|
@ -24,63 +79,26 @@ export const stripeRouter = createTRPCRouter({
|
|||
? `http://${req.headers.host ?? "localhost:3000"}`
|
||||
: `https://${req.headers.host ?? env.NEXTAUTH_URL}`;
|
||||
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
client_reference_id: session.user?.id,
|
||||
payment_method_types: ["card"],
|
||||
allow_promotion_codes: true,
|
||||
mode: "subscription",
|
||||
line_items: [
|
||||
{
|
||||
price: input.billedAnnually
|
||||
? env.STRIPE_ANNUAL_PRICE_ID
|
||||
: env.STRIPE_MONTHLY_PRICE_ID,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${baseUrl}/videos?checkoutSuccess=true`,
|
||||
cancel_url: `${baseUrl}/videos?checkoutCanceled=true`,
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
userId: session.user?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
const stripeBillingPortalSession =
|
||||
await stripe.billingPortal.sessions.create({
|
||||
customer: customerId,
|
||||
return_url: `${baseUrl}/videos`,
|
||||
});
|
||||
|
||||
if (!checkoutSession) {
|
||||
throw new Error("Could not create checkout session");
|
||||
if (!stripeBillingPortalSession) {
|
||||
throw new Error("Could not create billing portal session");
|
||||
}
|
||||
|
||||
return { checkoutUrl: checkoutSession.url };
|
||||
}),
|
||||
createBillingPortalSession: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
const { stripe, session, prisma, req } = ctx;
|
||||
|
||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||
prisma,
|
||||
stripe,
|
||||
userId: session.user?.id,
|
||||
});
|
||||
|
||||
if (!customerId) {
|
||||
throw new Error("Could not create customer");
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
env.NODE_ENV === "development"
|
||||
? `http://${req.headers.host ?? "localhost:3000"}`
|
||||
: `https://${req.headers.host ?? env.NEXTAUTH_URL}`;
|
||||
|
||||
const stripeBillingPortalSession =
|
||||
await stripe.billingPortal.sessions.create({
|
||||
customer: customerId,
|
||||
return_url: `${baseUrl}/videos`,
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "visiting billing portal page",
|
||||
properties: {
|
||||
stripeSubscriptionStatus: session.user.stripeSubscriptionStatus,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
|
||||
if (!stripeBillingPortalSession) {
|
||||
throw new Error("Could not create billing portal session");
|
||||
return { billingPortalUrl: stripeBillingPortalSession.url };
|
||||
}
|
||||
|
||||
return { billingPortalUrl: stripeBillingPortalSession.url };
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,34 +15,45 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export const videoRouter = createTRPCRouter({
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
const videos = await ctx.prisma.video.findMany({
|
||||
where: {
|
||||
userId: ctx.session.user.id,
|
||||
},
|
||||
});
|
||||
getAll: protectedProcedure.query(
|
||||
async ({ ctx: { prisma, session, s3, posthog } }) => {
|
||||
const videos = await prisma.video.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const videosWithThumbnailUrl = await Promise.all(
|
||||
videos.map(async (video) => {
|
||||
const thumbnailUrl = await getSignedUrl(
|
||||
ctx.s3,
|
||||
new GetObjectCommand({
|
||||
Bucket: env.AWS_BUCKET_NAME,
|
||||
Key: video.userId + "/" + video.id + "-thumbnail",
|
||||
})
|
||||
);
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "viewing video list",
|
||||
properties: {
|
||||
videoAmount: videos.length,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
|
||||
return { ...video, thumbnailUrl };
|
||||
})
|
||||
);
|
||||
const videosWithThumbnailUrl = await Promise.all(
|
||||
videos.map(async (video) => {
|
||||
const thumbnailUrl = await getSignedUrl(
|
||||
s3,
|
||||
new GetObjectCommand({
|
||||
Bucket: env.AWS_BUCKET_NAME,
|
||||
Key: video.userId + "/" + video.id + "-thumbnail",
|
||||
})
|
||||
);
|
||||
|
||||
return videosWithThumbnailUrl;
|
||||
}),
|
||||
return { ...video, thumbnailUrl };
|
||||
})
|
||||
);
|
||||
|
||||
return videosWithThumbnailUrl;
|
||||
}
|
||||
),
|
||||
get: publicProcedure
|
||||
.input(z.object({ videoId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { s3 } = ctx;
|
||||
const video = await ctx.prisma.video.findUnique({
|
||||
const { s3, posthog, session, prisma } = ctx;
|
||||
const video = await prisma.video.findUnique({
|
||||
where: {
|
||||
id: input.videoId,
|
||||
},
|
||||
|
|
@ -54,10 +65,27 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
if (video.userId !== ctx?.session?.user.id && !video.sharing) {
|
||||
if (video.userId !== session?.user.id && !video.sharing) {
|
||||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
if (session) {
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "viewing video",
|
||||
properties: {
|
||||
videoId: video.id,
|
||||
videoCreatedAt: video.createdAt,
|
||||
videoUpdatedAt: video.updatedAt,
|
||||
videoUser: video.user.id,
|
||||
videoSharing: video.sharing,
|
||||
videoDeleteAfterLinkExpires: video.delete_after_link_expires,
|
||||
videoShareLinkExpiresAt: video.shareLinkExpiresAt,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
}
|
||||
|
||||
const getObjectCommand = new GetObjectCommand({
|
||||
Bucket: env.AWS_BUCKET_NAME,
|
||||
Key: video.userId + "/" + video.id,
|
||||
|
|
@ -69,20 +97,29 @@ export const videoRouter = createTRPCRouter({
|
|||
}),
|
||||
getUploadUrl: protectedProcedure
|
||||
.input(z.object({ key: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
.mutation(async ({ ctx: { prisma, session, s3, posthog }, input }) => {
|
||||
const { key } = input;
|
||||
const { s3 } = ctx;
|
||||
|
||||
const videos = await ctx.prisma.video.findMany({
|
||||
const videos = await prisma.video.findMany({
|
||||
where: {
|
||||
userId: ctx.session.user.id,
|
||||
userId: session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
videos.length >= 10 &&
|
||||
ctx.session.user.stripeSubscriptionStatus !== "active"
|
||||
session.user.stripeSubscriptionStatus !== "active"
|
||||
) {
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "hit video upload limit",
|
||||
properties: {
|
||||
videoAmount: videos.length,
|
||||
stripeSubscriptionStatus: session.user.stripeSubscriptionStatus,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
|
|
@ -90,9 +127,19 @@ export const videoRouter = createTRPCRouter({
|
|||
});
|
||||
}
|
||||
|
||||
const video = await ctx.prisma.video.create({
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "uploading video",
|
||||
properties: {
|
||||
videoAmount: videos.length,
|
||||
stripeSubscriptionStatus: session.user.stripeSubscriptionStatus,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
|
||||
const video = await prisma.video.create({
|
||||
data: {
|
||||
userId: ctx.session.user.id,
|
||||
userId: session.user.id,
|
||||
title: key,
|
||||
},
|
||||
});
|
||||
|
|
@ -101,7 +148,7 @@ export const videoRouter = createTRPCRouter({
|
|||
s3,
|
||||
new PutObjectCommand({
|
||||
Bucket: env.AWS_BUCKET_NAME,
|
||||
Key: ctx.session.user.id + "/" + video.id,
|
||||
Key: session.user.id + "/" + video.id,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -122,11 +169,11 @@ export const videoRouter = createTRPCRouter({
|
|||
}),
|
||||
setSharing: protectedProcedure
|
||||
.input(z.object({ videoId: z.string(), sharing: z.boolean() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const updateVideo = await ctx.prisma.video.updateMany({
|
||||
.mutation(async ({ ctx: { prisma, session, posthog }, input }) => {
|
||||
const updateVideo = await prisma.video.updateMany({
|
||||
where: {
|
||||
id: input.videoId,
|
||||
userId: ctx.session.user.id,
|
||||
userId: session.user.id,
|
||||
},
|
||||
data: {
|
||||
sharing: input.sharing,
|
||||
|
|
@ -137,6 +184,16 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "update video setSharing",
|
||||
properties: {
|
||||
videoId: input.videoId,
|
||||
videoSharing: input.sharing,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updateVideo,
|
||||
|
|
@ -146,11 +203,11 @@ export const videoRouter = createTRPCRouter({
|
|||
.input(
|
||||
z.object({ videoId: z.string(), delete_after_link_expires: z.boolean() })
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const updateVideo = await ctx.prisma.video.updateMany({
|
||||
.mutation(async ({ ctx: { prisma, session, posthog }, input }) => {
|
||||
const updateVideo = await prisma.video.updateMany({
|
||||
where: {
|
||||
id: input.videoId,
|
||||
userId: ctx.session.user.id,
|
||||
userId: session.user.id,
|
||||
},
|
||||
data: {
|
||||
delete_after_link_expires: input.delete_after_link_expires,
|
||||
|
|
@ -161,6 +218,16 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "update video delete_after_link_expires",
|
||||
properties: {
|
||||
videoId: input.videoId,
|
||||
delete_after_link_expires: input.delete_after_link_expires,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updateVideo,
|
||||
|
|
@ -173,11 +240,11 @@ export const videoRouter = createTRPCRouter({
|
|||
shareLinkExpiresAt: z.nullable(z.date()),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const updateVideo = await ctx.prisma.video.updateMany({
|
||||
.mutation(async ({ ctx: { prisma, session, posthog }, input }) => {
|
||||
const updateVideo = await prisma.video.updateMany({
|
||||
where: {
|
||||
id: input.videoId,
|
||||
userId: ctx.session.user.id,
|
||||
userId: session.user.id,
|
||||
},
|
||||
data: {
|
||||
shareLinkExpiresAt: input.shareLinkExpiresAt,
|
||||
|
|
@ -188,6 +255,16 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "update video shareLinkExpiresAt",
|
||||
properties: {
|
||||
videoId: input.videoId,
|
||||
shareLinkExpiresAt: input.shareLinkExpiresAt,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updateVideo,
|
||||
|
|
@ -200,11 +277,11 @@ export const videoRouter = createTRPCRouter({
|
|||
title: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const updateVideo = await ctx.prisma.video.updateMany({
|
||||
.mutation(async ({ ctx: { prisma, session, posthog }, input }) => {
|
||||
const updateVideo = await prisma.video.updateMany({
|
||||
where: {
|
||||
id: input.videoId,
|
||||
userId: ctx.session.user.id,
|
||||
userId: session.user.id,
|
||||
},
|
||||
data: {
|
||||
title: input.title,
|
||||
|
|
@ -215,6 +292,16 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "update video title",
|
||||
properties: {
|
||||
videoId: input.videoId,
|
||||
title: input.title,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updateVideo,
|
||||
|
|
@ -226,11 +313,11 @@ export const videoRouter = createTRPCRouter({
|
|||
videoId: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const deleteVideo = await ctx.prisma.video.deleteMany({
|
||||
.mutation(async ({ ctx: { prisma, session, s3, posthog }, input }) => {
|
||||
const deleteVideo = await prisma.video.deleteMany({
|
||||
where: {
|
||||
id: input.videoId,
|
||||
userId: ctx.session.user.id,
|
||||
userId: session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -238,17 +325,26 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
const deleteVideoObject = await ctx.s3.send(
|
||||
posthog.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "video delete",
|
||||
properties: {
|
||||
videoId: input.videoId,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
|
||||
const deleteVideoObject = await s3.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: env.AWS_BUCKET_NAME,
|
||||
Key: ctx.session.user.id + "/" + input.videoId,
|
||||
Key: session.user.id + "/" + input.videoId,
|
||||
})
|
||||
);
|
||||
|
||||
const deleteThumbnailObject = await ctx.s3.send(
|
||||
const deleteThumbnailObject = await s3.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: env.AWS_BUCKET_NAME,
|
||||
Key: ctx.session.user.id + "/" + input.videoId + "-thumbnail",
|
||||
Key: session.user.id + "/" + input.videoId + "-thumbnail",
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
|||
session: opts.session,
|
||||
prisma,
|
||||
s3,
|
||||
posthog,
|
||||
stripe,
|
||||
req: opts.req,
|
||||
res: opts.res,
|
||||
|
|
@ -79,6 +80,7 @@ import { ZodError } from "zod";
|
|||
import { s3 } from "~/server/aws/s3";
|
||||
import { stripe } from "~/server/stripe";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { posthog } from "~/server/posthog";
|
||||
|
||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||
transformer: superjson,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import GitHubProvider from "next-auth/providers/github";
|
|||
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
||||
import { env } from "~/env.mjs";
|
||||
import { prisma } from "~/server/db";
|
||||
import { PostHog } from "posthog-node";
|
||||
|
||||
/**
|
||||
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
||||
|
|
@ -67,6 +68,44 @@ export const authOptions: NextAuthOptions = {
|
|||
* @see https://next-auth.js.org/providers/github
|
||||
*/
|
||||
],
|
||||
events: {
|
||||
async signIn(message) {
|
||||
const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
});
|
||||
|
||||
client.capture({
|
||||
distinctId: message.user.id,
|
||||
event: "user logged in",
|
||||
properties: {
|
||||
provider: message.account?.provider,
|
||||
isNewUser: message.isNewUser,
|
||||
},
|
||||
});
|
||||
|
||||
await client.shutdownAsync();
|
||||
},
|
||||
async signOut(message) {
|
||||
const session = message.session as unknown as {
|
||||
id: string;
|
||||
sessionToken: string;
|
||||
userId: string;
|
||||
expires: Date;
|
||||
};
|
||||
if (!session?.userId) return;
|
||||
|
||||
const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
});
|
||||
|
||||
client.capture({
|
||||
distinctId: session.userId,
|
||||
event: "user logged out",
|
||||
});
|
||||
|
||||
await client.shutdownAsync();
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/sign-in",
|
||||
},
|
||||
|
|
|
|||
6
src/server/posthog.ts
Normal file
6
src/server/posthog.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { PostHog } from "posthog-node";
|
||||
import { env } from "~/env.mjs";
|
||||
|
||||
export const posthog = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { PrismaClient } from "@prisma/client";
|
||||
import type Stripe from "stripe";
|
||||
import { type PostHog } from "posthog-node";
|
||||
|
||||
// retrieves a Stripe customer id for a given user if it exists or creates a new one
|
||||
export const getOrCreateStripeCustomerIdForUser = async ({
|
||||
|
|
@ -54,10 +55,12 @@ export const handleInvoicePaid = async ({
|
|||
event,
|
||||
stripe,
|
||||
prisma,
|
||||
posthog,
|
||||
}: {
|
||||
event: Stripe.Event;
|
||||
stripe: Stripe;
|
||||
prisma: PrismaClient;
|
||||
posthog: PostHog;
|
||||
}) => {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const subscriptionId = invoice.subscription;
|
||||
|
|
@ -76,14 +79,27 @@ export const handleInvoicePaid = async ({
|
|||
stripeSubscriptionStatus: subscription.status,
|
||||
},
|
||||
});
|
||||
|
||||
if (userId && subscription.status) {
|
||||
posthog.capture({
|
||||
distinctId: userId,
|
||||
event: "stripe invoice.paid",
|
||||
properties: {
|
||||
stripeSubscriptionStatus: subscription.status,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
}
|
||||
};
|
||||
|
||||
export const handleSubscriptionCreatedOrUpdated = async ({
|
||||
event,
|
||||
prisma,
|
||||
posthog,
|
||||
}: {
|
||||
event: Stripe.Event;
|
||||
prisma: PrismaClient;
|
||||
posthog: PostHog;
|
||||
}) => {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const userId = subscription.metadata.userId;
|
||||
|
|
@ -98,14 +114,24 @@ export const handleSubscriptionCreatedOrUpdated = async ({
|
|||
stripeSubscriptionStatus: subscription.status,
|
||||
},
|
||||
});
|
||||
|
||||
if (userId && subscription.status) {
|
||||
posthog.capture({
|
||||
distinctId: userId,
|
||||
event: "stripe subscription created or updated",
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
}
|
||||
};
|
||||
|
||||
export const handleSubscriptionCanceled = async ({
|
||||
event,
|
||||
prisma,
|
||||
posthog,
|
||||
}: {
|
||||
event: Stripe.Event;
|
||||
prisma: PrismaClient;
|
||||
posthog: PostHog;
|
||||
}) => {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const userId = subscription.metadata.userId;
|
||||
|
|
@ -120,4 +146,12 @@ export const handleSubscriptionCanceled = async ({
|
|||
stripeSubscriptionStatus: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (userId && subscription.status) {
|
||||
posthog.capture({
|
||||
distinctId: userId,
|
||||
event: "stripe subscription cancelled",
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue