diff --git a/apps/webapp/app/hooks/useOrganizations.ts b/apps/webapp/app/hooks/useOrganizations.ts index 2aa1e337001..efb6728a57b 100644 --- a/apps/webapp/app/hooks/useOrganizations.ts +++ b/apps/webapp/app/hooks/useOrganizations.ts @@ -1,3 +1,4 @@ +import { useCallback, useEffect, useRef, useState } from "react"; import type { UseDataFunctionReturn } from "remix-typedjson"; import type { loader as appLoader } from "~/routes/__app"; import type { loader as orgLoader } from "~/routes/__app/orgs/$organizationSlug"; diff --git a/apps/webapp/app/hooks/usePostHog.ts b/apps/webapp/app/hooks/usePostHog.ts new file mode 100644 index 00000000000..565dd6761be --- /dev/null +++ b/apps/webapp/app/hooks/usePostHog.ts @@ -0,0 +1,83 @@ +import { useLocation } from "@remix-run/react"; +import posthog from "posthog-js"; +import { useEffect, useRef } from "react"; +import { useCurrentEnvironment } from "./useEnvironments"; +import { useCurrentOrganization } from "./useOrganizations"; +import { useOptionalUser } from "./useUser"; +import { useCurrentWorkflow } from "./useWorkflows"; + +export const usePostHog = (apiKey?: string, logging = false): void => { + const postHogInitialized = useRef(false); + const location = useLocation(); + const user = useOptionalUser(); + const currentOrganization = useCurrentOrganization(); + const currentOrganizationId = currentOrganization?.id; + const currentWorkflow = useCurrentWorkflow(); + const currentWorkflowId = currentWorkflow?.id; + const currentEnvironment = useCurrentEnvironment(); + const currentEnvironmentId = currentEnvironment?.id; + + //start PostHog once + useEffect(() => { + if (apiKey === undefined) return; + if (postHogInitialized.current === true) return; + if (logging) console.log("posthog.init", apiKey); + postHogInitialized.current = true; + posthog.init(apiKey, { + api_host: "https://app.posthog.com", + loaded: function (posthog) { + if (logging) console.log("posthog.loaded", apiKey); + if (user !== undefined) { + if (logging) console.log("posthog.identify", user.id, user.email); + posthog.identify(user.id, { email: user.email }); + } + }, + }); + }, [apiKey, logging, user]); + + //identify the user, and unidentify on log out + useEffect(() => { + if (postHogInitialized.current === false) return; + if (user === undefined) { + if (logging) console.log("posthog.reset"); + posthog.reset(); + } else { + if (logging) console.log("posthog.identify", user.id, user.email); + posthog.identify(user.id, { email: user.email }); + } + }, [logging, user]); + + //identify the organization + useEffect(() => { + if (postHogInitialized.current === false) return; + if (currentOrganizationId !== undefined) { + if (logging) console.log("posthog.organization", currentOrganizationId); + posthog.group("organization", currentOrganizationId); + } + }, [currentOrganizationId, logging]); + + //identify the workflow + useEffect(() => { + if (postHogInitialized.current === false) return; + if (currentWorkflowId !== undefined) { + if (logging) console.log("posthog.workflow", currentWorkflowId); + posthog.group("workflow", currentWorkflowId); + } + }, [currentWorkflowId, logging]); + + //identify the environment + useEffect(() => { + if (postHogInitialized.current === false) return; + if (currentEnvironmentId !== undefined) { + if (logging) console.log("posthog.environment", currentEnvironmentId); + posthog.group("environment", currentEnvironmentId); + } + }, [currentEnvironmentId, logging]); + + //page view + useEffect(() => { + if (postHogInitialized.current === false) return; + if (logging) console.log("posthog.capture", "$pageview", location.pathname); + posthog.capture("$pageview"); + }, [location, logging]); +}; diff --git a/apps/webapp/app/models/organization.server.ts b/apps/webapp/app/models/organization.server.ts index 43b6a96eb5e..14b13230be2 100644 --- a/apps/webapp/app/models/organization.server.ts +++ b/apps/webapp/app/models/organization.server.ts @@ -3,6 +3,7 @@ import { prisma } from "~/db.server"; import slug from "slug"; import { customAlphabet } from "nanoid"; import { generateTwoRandomWords } from "~/utils/randomWords"; +import { taskQueue } from "~/services/messageBroker.server"; export type { Organization } from ".prisma/client"; @@ -34,13 +35,7 @@ export function getOrganizationFromSlug({ { title: "asc" }, ], }, - environments: { - select: { - id: true, - slug: true, - apiKey: true, - }, - }, + environments: true, }, where: { slug, users: { some: { id: userId } } }, }); @@ -130,6 +125,10 @@ export async function createOrganization({ await createEnvironment(organization, "development"); await createEnvironment(organization, "live"); + await taskQueue.publish("ORGANIZATION_CREATED", { + id: organization.id, + }); + return organization; } diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index 83b085f73e4..92fef4939a0 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -8,7 +8,6 @@ import { ScrollRestoration, useCatch, } from "@remix-run/react"; - import tailwindStylesheetUrl from "./styles/tailwind.css"; import prismStylesheetUrl from "./styles/prism.css"; import prismThemeStylesheetUrl from "./styles/prism-trigger-theme.css"; @@ -17,12 +16,12 @@ import { Toaster, toast } from "react-hot-toast"; import type { ToastMessage } from "~/models/message.server"; import { commitSession, getSession } from "~/models/message.server"; import { useEffect, useRef } from "react"; -import posthog from "posthog-js"; import { withSentry } from "@sentry/remix"; import { env } from "./env.server"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { PrimaryLink } from "./components/primitives/Buttons"; import { Body } from "./components/primitives/text/Body"; +import { usePostHog } from "./hooks/usePostHog"; export const links: LinksFunction = () => { return [ @@ -63,7 +62,7 @@ export function CatchBoundary() { -
+

{caught.status} {caught.statusText} @@ -89,7 +88,7 @@ export function ErrorBoundary({ error }: { error: any }) { -
+

Oh no!

{JSON.stringify(error)} @@ -105,9 +104,9 @@ export function ErrorBoundary({ error }: { error: any }) { } function App() { - const { toastMessage, posthogProjectKey, user } = + const { toastMessage, posthogProjectKey } = useTypedLoaderData(); - const postHogInitialised = useRef(false); + usePostHog(posthogProjectKey); useEffect(() => { if (!toastMessage) { @@ -127,30 +126,6 @@ function App() { } }, [toastMessage]); - useEffect(() => { - if (posthogProjectKey !== undefined) { - posthog.init(posthogProjectKey, { - api_host: "https://app.posthog.com", - loaded: function (posthog) { - if (user !== null) { - posthog.identify(user.id, { email: user.email }); - } - }, - }); - postHogInitialised.current = true; - } - }); - - useEffect(() => { - if (postHogInitialised.current) { - if (user === null) { - posthog.reset(); - } else { - posthog.identify(user.id, { email: user.email }); - } - } - }, [user]); - return ( diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug.tsx index 1fbfdce5e27..6d7a514669a 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug.tsx @@ -5,6 +5,7 @@ import { Outlet } from "@remix-run/react"; import { getOrganizationFromSlug } from "~/models/organization.server"; import { typedjson } from "remix-typedjson"; import { getRuntimeEnvironmentFromRequest } from "~/models/runtimeEnvironment.server"; +import { analytics } from "~/services/analytics.server"; export const loader = async ({ request, params }: LoaderArgs) => { const userId = await requireUserId(request); @@ -20,11 +21,26 @@ export const loader = async ({ request, params }: LoaderArgs) => { throw new Response("Not Found", { status: 404 }); } + analytics.organization.identify({ organization }); + const currentEnvironmentSlug = await getRuntimeEnvironmentFromRequest( request ); + const currentEnvironment = organization.environments?.find( + (e) => e.slug === currentEnvironmentSlug + ); - return typedjson({ organization, currentEnvironmentSlug }); + if (currentEnvironment == null) { + throw new Response("Not Found", { status: 404 }); + } + + analytics.environment.identify({ environment: currentEnvironment }); + + return typedjson({ + organization, + currentEnvironment, + currentEnvironmentSlug, + }); }; export default function Organization() { diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx index c20526f4aff..6c243791bf4 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx @@ -16,6 +16,7 @@ import { buildExternalSourceUrl } from "~/models/externalSource.server"; import { getIntegrations } from "~/models/integrations.server"; import { getRuntimeEnvironmentFromRequest } from "~/models/runtimeEnvironment.server"; import { getWorkflowFromSlugs } from "~/models/workflow.server"; +import { analytics } from "~/services/analytics.server"; import { requireUser } from "~/services/session.server"; type ExternalSourceConfig = @@ -59,6 +60,8 @@ export const loader = async ({ request, params }: LoaderArgs) => { throw new Response("Not Found", { status: 404 }); } + analytics.workflow.identify({ workflow }); + const integrations = getIntegrations(user.admin); const rules = workflow.rules.map((r) => ({ @@ -66,10 +69,6 @@ export const loader = async ({ request, params }: LoaderArgs) => { trigger: TriggerMetadataSchema.parse(r.trigger), })); - const currentEnvironmentSlug = await getRuntimeEnvironmentFromRequest( - request - ); - const allConnections = await getConnectedApiConnectionsForOrganizationSlug({ slug: organizationSlug, }); diff --git a/apps/webapp/app/routes/__app/orgs/new.tsx b/apps/webapp/app/routes/__app/orgs/new.tsx index d1063d995bf..79a5814c832 100644 --- a/apps/webapp/app/routes/__app/orgs/new.tsx +++ b/apps/webapp/app/routes/__app/orgs/new.tsx @@ -49,8 +49,8 @@ export default function NewOrganizationPage() { }, [actionData]); return ( -
-
+
+
Create a new Organization @@ -63,13 +63,13 @@ export default function NewOrganizationPage() { width: "100%", }} > -
+
-