diff --git a/.prettierignore b/.prettierignore index a153f02d..9b7334e8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,7 @@ bun.lockb drizzle/ .prettierignore .gitignore +*.env.example .changeset/ diff --git a/apps/docs/installation.mdx b/apps/docs/installation.mdx index b714c4d8..c1af1b74 100644 --- a/apps/docs/installation.mdx +++ b/apps/docs/installation.mdx @@ -112,6 +112,14 @@ To obtain this key, head over to your unkey dashboard and create a new ratelimit As for the namespace that you have created, you will need to put that as the value of the `UNKEY_NAMESPACE` environment variable. +#### Posthog (optional) + +[Posthog](https://posthog.com) is the service of choice in OrbitKit for analysis, it is however optional as that if the `NEXT_PUBLIC_POSTHOG_HOST` or `NEXT_PUBLIC_POSTHOG_KEY` variables are not supplied for the web app or `PUBLIC_POSTHOG_KEY` and `PUBLIC_POSTHOG_HOST` for the marketing app, no analysis is applied. + +To obtain this key, head over to your posthog dashboard, then in your settings, find `Project API Key`, that will be the value of the key environment variable. + +As for the host use `app.posthog.com` or `eu.posthog.com` depending on your region. + #### OAuth OrbitKit currently ships with two authentication providers, Google and Github. You will need to create an OAuth application on the respective platforms and input the values into the `.env.local` file. diff --git a/apps/marketing/.env.example b/apps/marketing/.env.example new file mode 100644 index 00000000..c71fd3dc --- /dev/null +++ b/apps/marketing/.env.example @@ -0,0 +1,2 @@ +PUBLIC_POSTHOG_KEY= +PUBLIC_POSTHOG_HOST= \ No newline at end of file diff --git a/apps/marketing/package.json b/apps/marketing/package.json index c146a420..f800b7ab 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -23,6 +23,7 @@ "@t3-oss/env-core": "^0.10.1", "@total-typescript/ts-reset": "^0.5.1", "astro": "^4.10.1", + "posthog-js": "^1.139.0", "react": "^18.3.1", "react-dom": "^18.3.1", "zod": "^3.23.8" diff --git a/apps/marketing/src/components/posthog/PostHog.astro b/apps/marketing/src/components/posthog/PostHog.astro new file mode 100644 index 00000000..7946c11f --- /dev/null +++ b/apps/marketing/src/components/posthog/PostHog.astro @@ -0,0 +1 @@ + diff --git a/apps/marketing/src/components/posthog/posthog.ts b/apps/marketing/src/components/posthog/posthog.ts new file mode 100644 index 00000000..0667b05e --- /dev/null +++ b/apps/marketing/src/components/posthog/posthog.ts @@ -0,0 +1,8 @@ +import { env } from '@orbitkit/env/marketing'; +import posthog from 'posthog-js'; + +if (env.PUBLIC_POSTHOG_KEY && env.PUBLIC_POSTHOG_HOST) { + posthog.init(env.PUBLIC_POSTHOG_KEY, { + api_host: env.PUBLIC_POSTHOG_HOST, + }); +} diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro index a196da71..76121e01 100644 --- a/apps/marketing/src/pages/index.astro +++ b/apps/marketing/src/pages/index.astro @@ -1,12 +1,14 @@ --- import { MyAvatar } from '@/components/avatar'; import BaseHead from '@/components/BaseHead.astro'; +import PostHog from '@/components/posthog/PostHog.astro'; --- +
diff --git a/apps/marketing/turbo.json b/apps/marketing/turbo.json index 93e17844..718de42f 100644 --- a/apps/marketing/turbo.json +++ b/apps/marketing/turbo.json @@ -6,7 +6,8 @@ "dependsOn": ["^build", "build"] }, "build": { - "outputs": [".astro/**", "dist/**"] + "outputs": [".astro/**", "dist/**"], + "passThroughEnv": ["PUBLIC_POSTHOG_KEY", "PUBLIC_POSTHOG_HOST"] } } } diff --git a/apps/web/.env.example b/apps/web/.env.example index 470248ad..233e0618 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -5,6 +5,10 @@ DATABASE_URL= UPLOADTHING_SECRET= UPLOADTHING_APP_ID= +# Posthog (optional) +NEXT_PUBLIC_POSTHOG_HOST= +NEXT_PUBLIC_POSTHOG_KEY= + # Unkey (optional) UNKEY_NAMESPACE= UNKEY_ROOT_KEY= diff --git a/apps/web/package.json b/apps/web/package.json index a1a386ad..d982d208 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -33,6 +33,8 @@ "geist": "^1.3.0", "next": "14.2.3", "next-themes": "^0.3.0", + "posthog-js": "^1.139.0", + "posthog-node": "^4.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "server-only": "^0.0.1", diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 3df1dc0d..1de6a3b6 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -3,6 +3,7 @@ import './globals.css'; import { ThemeProvider } from 'next-themes'; import type { Metadata } from 'next'; +import dynamic from 'next/dynamic'; import { NextSSRPlugin } from '@uploadthing/react/next-ssr-plugin'; import { GeistMono } from 'geist/font/mono'; @@ -15,6 +16,12 @@ import { TRPCReactProvider } from '@/lib/trpc/react'; import { fileRouter } from './api/uploadthing/core'; +import { PostHogReactProvider } from '@/lib/posthog/react'; + +const PostHogPageView = dynamic(() => import('@/lib/posthog/view'), { + ssr: false, +}); + export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', @@ -33,13 +40,20 @@ export default function RootLayout({ }>) { return ( - - - - {children} - - - + + + + + + {children} + + + + ); } diff --git a/apps/web/src/lib/posthog/client.ts b/apps/web/src/lib/posthog/client.ts new file mode 100644 index 00000000..9ef38832 --- /dev/null +++ b/apps/web/src/lib/posthog/client.ts @@ -0,0 +1,18 @@ +import { env } from '@orbitkit/env/web'; +import { PostHog } from 'posthog-node'; + +/** + * This component is used to initialize posthog client. + * @returns posthog client. + */ +export default function PostHogClient() { + if (!env.NEXT_PUBLIC_POSTHOG_KEY || !env.NEXT_PUBLIC_POSTHOG_HOST) { + return; + } + + return new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, { + host: env.NEXT_PUBLIC_POSTHOG_HOST, + flushAt: 1, // Sets how many capture calls we should flush the queue (in one batch). + flushInterval: 0, // Sets how many milliseconds we should wait before flushing the queue + }); +} diff --git a/apps/web/src/lib/posthog/react.tsx b/apps/web/src/lib/posthog/react.tsx new file mode 100644 index 00000000..fa15961c --- /dev/null +++ b/apps/web/src/lib/posthog/react.tsx @@ -0,0 +1,29 @@ +'use client'; +import { env } from '@orbitkit/env/web'; +import posthog from 'posthog-js'; +import { PostHogProvider } from 'posthog-js/react'; + +if ( + typeof window !== 'undefined' && + env.NEXT_PUBLIC_POSTHOG_HOST && + env.NEXT_PUBLIC_POSTHOG_KEY +) { + posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: env.NEXT_PUBLIC_POSTHOG_HOST, + capture_pageview: false, // Disable automatic pageview capture, as we capture manually + }); +} + +/** + * This component is used to initialize the posthog on the client side. + * @param props The props to the component. + * @param props.children The children to render. + * @returns posthog provider wrapper around the children. + */ +export function PostHogReactProvider({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/apps/web/src/lib/posthog/view.tsx b/apps/web/src/lib/posthog/view.tsx new file mode 100644 index 00000000..5d6e0c0d --- /dev/null +++ b/apps/web/src/lib/posthog/view.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { usePathname, useSearchParams } from 'next/navigation'; +import { useEffect } from 'react'; +import { type PostHog, usePostHog } from 'posthog-js/react'; + +/** + * This component is used to capture page views in PostHog. + * @returns null + */ +export default function PostHogPageView(): null { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const posthog = usePostHog() as PostHog | undefined; + + useEffect(() => { + if (pathname && posthog) { + let url = window.origin + pathname; + if (searchParams.toString()) { + url = url + `?${searchParams.toString()}`; + } + posthog.capture('$pageview', { + $current_url: url, + }); + } + }, [pathname, searchParams, posthog]); + + return null; +} diff --git a/apps/web/turbo.json b/apps/web/turbo.json index d6d67df0..2e0058a2 100644 --- a/apps/web/turbo.json +++ b/apps/web/turbo.json @@ -16,7 +16,9 @@ "AUTH_GITHUB_SECRET", "AUTH_GOOGLE_ID", "AUTH_GOOGLE_SECRET", - "AUTH_GOOGLE_CODE_VERIFIER" + "AUTH_GOOGLE_CODE_VERIFIER", + "NEXT_PUBLIC_POSTHOG_HOST", + "NEXT_PUBLIC_POSTHOG_KEY" ] }, "test:e2e": { @@ -30,7 +32,9 @@ "AUTH_GITHUB_SECRET", "AUTH_GOOGLE_ID", "AUTH_GOOGLE_SECRET", - "AUTH_GOOGLE_CODE_VERIFIER" + "AUTH_GOOGLE_CODE_VERIFIER", + "NEXT_PUBLIC_POSTHOG_HOST", + "NEXT_PUBLIC_POSTHOG_KEY" ] } } diff --git a/cspell.config.yaml b/cspell.config.yaml index 5432c68a..104aff91 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -5,6 +5,7 @@ ignorePaths: - bun.lockb - ./branch_out - .tsbuildinfo + - '*.tsbuildinfo' - .gitignore - dist - storybook-static @@ -61,7 +62,9 @@ words: - Ornella - packagejson - pacocoursey + - pageview - peduarte + - posthog - quickstart - Ratelimit - shadcn diff --git a/packages/env/src/marketing/index.ts b/packages/env/src/marketing/index.ts index a0de81ad..d69484cf 100644 --- a/packages/env/src/marketing/index.ts +++ b/packages/env/src/marketing/index.ts @@ -1,10 +1,14 @@ import { createEnv } from '@t3-oss/env-core'; +import { z } from 'zod'; import { sharedEnv } from '../shared'; export const env = createEnv({ extends: [sharedEnv], server: {}, - client: {}, + client: { + PUBLIC_POSTHOG_KEY: z.string().optional(), + PUBLIC_POSTHOG_HOST: z.string().optional(), + }, clientPrefix: 'PUBLIC_', runtimeEnv: import.meta.env, skipValidation: !!import.meta.env.SKIP_ENV_VALIDATION, diff --git a/packages/env/src/web/index.ts b/packages/env/src/web/index.ts index 3834e4ac..d242bfb1 100644 --- a/packages/env/src/web/index.ts +++ b/packages/env/src/web/index.ts @@ -26,9 +26,16 @@ export const env = createEnv({ AUTH_GOOGLE_SECRET: z.string().optional(), AUTH_GOOGLE_CODE_VERIFIER: z.string().optional(), }, - client: {}, + client: { + NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), + NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(), + }, experimental__runtimeEnv: { NODE_ENV: process.env.NODE_ENV, + // eslint-disable-next-line turbo/no-undeclared-env-vars + NEXT_PUBLIC_POSTHOG_KEY: process.env['NEXT_PUBLIC_POSTHOG_KEY'], + // eslint-disable-next-line turbo/no-undeclared-env-vars + NEXT_PUBLIC_POSTHOG_HOST: process.env['NEXT_PUBLIC_POSTHOG_HOST'], }, emptyStringAsUndefined: true, skipValidation: !!process.env['SKIP_ENV_VALIDATION'],