diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3e4402295..7788c494b 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,7 +4,8 @@ // List of extensions which should be recommended for users of this workspace. "recommendations": [ - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + "streetsidesoftware.code-spell-checker" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [ diff --git a/.vscode/settings.json b/.vscode/settings.json index cb0f783fa..1ca960233 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,6 +28,8 @@ "nicify", "oidc", "openapi", + "pageleave", + "pageview", "posthog", "Proxied", "qrcode", diff --git a/apps/backend/package.json b/apps/backend/package.json index d15ebafab..4ea091e18 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -39,7 +39,7 @@ "openid-client": "^5.6.4", "oslo": "^1.2.1", "pg": "^8.11.3", - "posthog-js": "^1.149.1", + "posthog-node": "^4.1.0", "react": "^18.2", "server-only": "^0.0.1", "sharp": "^0.32.6", diff --git a/apps/backend/src/analytics.tsx b/apps/backend/src/analytics.tsx new file mode 100644 index 000000000..6b29f1e3c --- /dev/null +++ b/apps/backend/src/analytics.tsx @@ -0,0 +1,16 @@ +import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env'; +import { PostHog } from 'posthog-node'; + +export default async function withPostHog(callback: (posthog: PostHog) => Promise) { + const postHogKey = getEnvVariable("NEXT_PUBLIC_POSTHOG_KEY", "phc_vIUFi0HzHo7oV26OsaZbUASqxvs8qOmap1UBYAutU4k"); + const posthogClient = new PostHog(postHogKey, { + host: "https://eu.i.posthog.com", + flushAt: 1, + flushInterval: 0 + }); + try { + await callback(posthogClient); + } finally { + await posthogClient.shutdown(); + } +} diff --git a/apps/backend/src/lib/events.tsx b/apps/backend/src/lib/events.tsx index 06b008d98..0ae56b49d 100644 --- a/apps/backend/src/lib/events.tsx +++ b/apps/backend/src/lib/events.tsx @@ -4,7 +4,8 @@ import * as yup from "yup"; import { UnionToIntersection } from "@stackframe/stack-shared/dist/utils/types"; import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { prismaClient } from "@/prisma-client"; -import posthog from "posthog-js"; +import withPostHog from "@/analytics"; +import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; type EventType = { id: string, @@ -137,13 +138,21 @@ export async function logEvent( }); // log event in PostHog - for (const eventType of allEventTypes) { - const postHogEventName = `stack_${eventType.id.replace(/^\$/, "system_").replace(/-/g, "_")}`; - posthog.capture(postHogEventName, { - data, - isWide, - eventStartedAt: timeRange.start, - eventEndedAt: timeRange.end, - }); - } + await withPostHog(async posthog => { + const distinctId = typeof data === "object" && data && "userId" in data ? (data.userId as string) : `backend-anon-${generateUuid()}`; + for (const eventType of allEventTypes) { + const postHogEventName = `stack_${eventType.id.replace(/^\$/, "system_").replace(/-/g, "_")}`; + posthog.capture({ + event: postHogEventName, + distinctId, + timestamp: timeRange.end, + properties: { + data, + is_wide: isWide, + event_started_at: timeRange.start, + event_ended_at: timeRange.end, + }, + }); + } + }); } diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index b8339005c..50a163d33 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -80,6 +80,10 @@ const nextConfig = { source: "/consume/:path*", destination: "https://eu.i.posthog.com/:path*", }, + { + source: "/consume/decide", + destination: "https://eu.i.posthog.com/decide", + }, ]; }, skipTrailingSlashRedirect: true, diff --git a/apps/dashboard/src/app/layout.tsx b/apps/dashboard/src/app/layout.tsx index 51b73653e..683268772 100644 --- a/apps/dashboard/src/app/layout.tsx +++ b/apps/dashboard/src/app/layout.tsx @@ -17,6 +17,7 @@ import '../polyfills'; import { ClientPolyfill } from './client-polyfill'; import './globals.css'; import { CSPostHogProvider, UserIdentity } from './providers'; +import dynamic from 'next/dynamic'; export const metadata: Metadata = { metadataBase: new URL(process.env.NEXT_PUBLIC_STACK_URL_DEPRECATED || ''), @@ -41,6 +42,10 @@ type TagConfigJson = { innerHTML?: string, }; +const PageView = dynamic(() => import('./pageview'), { + ssr: false, +}); + export default function RootLayout({ children, }: { @@ -73,6 +78,7 @@ export default function RootLayout({ suppressHydrationWarning > + diff --git a/apps/dashboard/src/app/pageview.tsx b/apps/dashboard/src/app/pageview.tsx new file mode 100644 index 000000000..e0986260c --- /dev/null +++ b/apps/dashboard/src/app/pageview.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { usePathname, useSearchParams } from "next/navigation"; +import { useEffect } from "react"; +import { usePostHog } from 'posthog-js/react'; + +export default function PostHogPageView(): null { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const posthog = usePostHog(); + useEffect(() => { + if (pathname) { + 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/dashboard/src/app/providers.tsx b/apps/dashboard/src/app/providers.tsx index aedc9ed36..0bde24c05 100644 --- a/apps/dashboard/src/app/providers.tsx +++ b/apps/dashboard/src/app/providers.tsx @@ -10,6 +10,8 @@ if (typeof window !== 'undefined') { posthog.init(postHogKey, { api_host: "/consume", ui_host: "https://eu.i.posthog.com", + capture_pageview: false, + capture_pageleave: true, }); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31c0f9a5e..e13173164 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,9 +125,9 @@ importers: pg: specifier: ^8.11.3 version: 8.12.0 - posthog-js: - specifier: ^1.149.1 - version: 1.149.2 + posthog-node: + specifier: ^4.1.0 + version: 4.1.0 react: specifier: ^18.2 version: 18.3.1 @@ -4108,6 +4108,9 @@ packages: resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} engines: {node: '>=4'} + axios@1.7.4: + resolution: {integrity: sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==} + axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -5216,6 +5219,15 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -6938,6 +6950,10 @@ packages: posthog-js@1.149.2: resolution: {integrity: sha512-4tNtVJkq3wZ5CvfOEp3Jtl/r3ogZb5To+bdu7JoO5QjkpTY9TV1pfo/Ag4keODpAzRDahC8OaCoIr4mY3dSK4g==} + posthog-node@4.1.0: + resolution: {integrity: sha512-Fd+aMWLjUttlPrfOniDWs35v62rOEIqP5GBzUvRswsNY8rr1g1KuDobqaRFGMCNnrtDmhzUN8y7QucrcwMY/+w==} + engines: {node: '>=15.0.0'} + preact@10.22.0: resolution: {integrity: sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==} @@ -7301,6 +7317,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rusha@0.8.14: + resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==} + rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} @@ -11698,6 +11717,14 @@ snapshots: axe-core@4.7.0: {} + axios@1.7.4: + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@3.2.1: dependencies: dequal: 2.0.3 @@ -13038,6 +13065,8 @@ snapshots: flatted@3.3.1: {} + follow-redirects@1.15.6: {} + for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -15160,6 +15189,13 @@ snapshots: preact: 10.22.0 web-vitals: 4.2.2 + posthog-node@4.1.0: + dependencies: + axios: 1.7.4 + rusha: 0.8.14 + transitivePeerDependencies: + - debug + preact@10.22.0: {} prebuild-install@7.1.2: @@ -15628,6 +15664,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rusha@0.8.14: {} + rxjs@7.8.1: dependencies: tslib: 2.6.3