Skip to content

Commit

Permalink
Add analytics to homepage (#5278)
Browse files Browse the repository at this point in the history
* feature: add posthog analytics to homepage

* add posthog user identify

* TEMPORARY enable posthog on dev

* capture all primary events

* Revert "TEMPORARY enable posthog on dev"

This reverts commit a92531b.

* changesets

* use posthog's distinctId

* fix posthog distinc id

* change events for better aggregation

* track on step complete

* track all completed button

* type fixes

* introduce useAnalytics hook

* rename hook and capture fn

---------

Co-authored-by: M.Graczyk <[email protected]>
  • Loading branch information
Cloud11PL and michalina-graczyk authored Nov 29, 2024
1 parent 4add531 commit c3e02b9
Show file tree
Hide file tree
Showing 17 changed files with 151 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-dogs-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

Environments created via Saleor Cloud now identify and report to PostHog. This means Dashboard now sends telemetry data regarding home page onboarding steps and links.
4 changes: 4 additions & 0 deletions src/auth/hooks/useAuthProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ApolloClient, ApolloError } from "@apollo/client";
import { IMessageContext } from "@dashboard/components/messages";
import { useAnalytics } from "@dashboard/components/ProductAnalytics/useAnalytics";
import { DEMO_MODE } from "@dashboard/config";
import { useUserDetailsQuery } from "@dashboard/graphql";
import useLocalStorage from "@dashboard/hooks/useLocalStorage";
Expand Down Expand Up @@ -36,6 +37,7 @@ export interface UseAuthProviderOpts {

export function useAuthProvider({ intl, notify, apolloClient }: UseAuthProviderOpts): UserContext {
const { login, getExternalAuthUrl, getExternalAccessToken, logout } = useAuth();
const analytics = useAnalytics();
const navigate = useNavigator();
const { authenticated, authenticating, user } = useAuthState();
const [requestedExternalPluginId] = useLocalStorage("requestedExternalPluginId", null);
Expand Down Expand Up @@ -81,6 +83,8 @@ export function useAuthProvider({ intl, notify, apolloClient }: UseAuthProviderO
}
};
const handleLogout = async () => {
analytics.reset();

const returnTo = urlJoin(window.location.origin, getAppMountUriForRedirect());
const result = await logout({
input: JSON.stringify({
Expand Down
6 changes: 5 additions & 1 deletion src/components/ProductAnalytics/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PostHogConfig } from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import React from "react";

Expand All @@ -7,7 +8,10 @@ const useConfig = () => {
capture_pageview: false,
autocapture: false,
advanced_disable_decide: true,
};
loaded: posthog => {
if (process.env.NODE_ENV === "development") posthog.debug();
},
} satisfies Partial<PostHogConfig>;
const apiKey = process.env.POSTHOG_KEY;
const isCloudInstance = process.env.IS_CLOUD_INSTANCE;
const canRenderAnalytics = () => {
Expand Down
28 changes: 28 additions & 0 deletions src/components/ProductAnalytics/useAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { usePostHog } from "posthog-js/react";

export function useAnalytics() {
const posthog = usePostHog();

function initialize(details: Record<string, any>) {
// According to docs, posthog can be briefly undefined
if (!posthog) return;

const id = posthog.get_distinct_id();

posthog.identify(id, details);
}

function reset() {
if (!posthog) return;

posthog.reset();
}

function trackEvent(event: string, properties?: Record<string, any>) {
if (!posthog) return;

posthog.capture(event, properties);
}

return { initialize, reset, trackEvent };
}
2 changes: 2 additions & 0 deletions src/components/ProductAnalytics/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const extractEmailDomain = (email: string | undefined) =>
email?.split("@")[1] ?? "internal-no-domain";
18 changes: 17 additions & 1 deletion src/components/Shop/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,35 @@ import favicon32 from "@assets/favicons/favicon-32x32.png";
import safariPinnedTab from "@assets/favicons/safari-pinned-tab.svg";
import { useUser } from "@dashboard/auth";
import { ShopInfoQuery, useShopInfoQuery } from "@dashboard/graphql";
import React from "react";
import React, { useEffect } from "react";
import Helmet from "react-helmet";

import { useAnalytics } from "../ProductAnalytics/useAnalytics";
import { extractEmailDomain } from "../ProductAnalytics/utils";

type ShopContext = ShopInfoQuery["shop"];

export const ShopContext = React.createContext<ShopContext>(undefined);

export const ShopProvider: React.FC = ({ children }) => {
const { authenticated, user } = useUser();
const analytics = useAnalytics();
const { data } = useShopInfoQuery({
skip: !authenticated || !user,
});

useEffect(() => {
if (data) {
const { shop } = data;

analytics.initialize({
domain: shop.domain.host,
email_domain: extractEmailDomain(user.email),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);

return (
<>
<Helmet>
Expand Down
24 changes: 12 additions & 12 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,25 +116,25 @@ const App: React.FC = () => (
<BackgroundTasksProvider>
<AppStateProvider>
<AuthProvider>
<ShopProvider>
<AppChannelProvider>
<ExitFormDialogProvider>
<DevModeProvider>
<NavigatorSearchProvider>
<ProductAnalytics>
<ProductAnalytics>
<ShopProvider>
<AppChannelProvider>
<ExitFormDialogProvider>
<DevModeProvider>
<NavigatorSearchProvider>
<SavebarRefProvider>
<FeatureFlagsProviderWithUser>
<OnboardingProvider>
<Routes />
</OnboardingProvider>
</FeatureFlagsProviderWithUser>
</SavebarRefProvider>
</ProductAnalytics>
</NavigatorSearchProvider>
</DevModeProvider>
</ExitFormDialogProvider>
</AppChannelProvider>
</ShopProvider>
</NavigatorSearchProvider>
</DevModeProvider>
</ExitFormDialogProvider>
</AppChannelProvider>
</ShopProvider>
</ProductAnalytics>
</AuthProvider>
</AppStateProvider>
</BackgroundTasksProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { Button } from "@saleor/macaw-ui-next";
import React from "react";
import { FormattedMessage } from "react-intl";

export const WelcomePageCheckGraphQLButton = () => {
import { PrimaryActionProps } from "./type";

export const WelcomePageCheckGraphQLButton = ({ onClick }: PrimaryActionProps) => {
const context = useDevModeContext();

return (
<Button
variant="primary"
onClick={() => {
onClick();
context.setDevModeVisibility(true);
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import React from "react";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";

import { PrimaryActionProps } from "./type";
import { WelcomePageFakeDisabledButton } from "./WelcomePageFakeDisabledButton";

export const WelcomePageCreateProductButton = () => {
export const WelcomePageCreateProductButton = ({ onClick }: PrimaryActionProps) => {
const { user } = useUser();
const userPermissions = user?.userPermissions || [];
const hasPermissionToManageProducts = hasPermissions(userPermissions, [
Expand Down Expand Up @@ -40,7 +41,7 @@ export const WelcomePageCreateProductButton = () => {
}

return (
<Link to={productListUrl()}>
<Link to={productListUrl()} onClick={onClick}>
<Button variant="primary">
<FormattedMessage defaultMessage="Go to all products" id="XZpRr8" description="btn label" />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import React from "react";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";

import { PrimaryActionProps } from "./type";
import { WelcomePageFakeDisabledButton } from "./WelcomePageFakeDisabledButton";

export const WelcomePageInviteStaffButton = () => {
export const WelcomePageInviteStaffButton = ({ onClick }: PrimaryActionProps) => {
const { user } = useUser();
const userPermissions = user?.userPermissions || [];
const hasPermissionToManageStaff = hasPermissions(userPermissions, [PermissionEnum.MANAGE_STAFF]);
Expand All @@ -35,7 +36,7 @@ export const WelcomePageInviteStaffButton = () => {
}

return (
<Link to={staffListUrl({ action: "add" })}>
<Link to={staffListUrl({ action: "add" })} onClick={onClick}>
<Button variant="primary">
<FormattedMessage defaultMessage="Invite members" id="BBt3jD" description="btn label" />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import React from "react";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";

import { PrimaryActionProps } from "./type";
import { WelcomePageFakeDisabledButton } from "./WelcomePageFakeDisabledButton";

export const WelcomePageOrdersButton = () => {
export const WelcomePageOrdersButton = ({ onClick }: PrimaryActionProps) => {
const { user } = useUser();
const userPermissions = user?.userPermissions || [];
const hasPermissionToManageOrders = hasPermissions(userPermissions, [
Expand All @@ -36,7 +37,7 @@ export const WelcomePageOrdersButton = () => {
}

return (
<Link to={orderListUrl()}>
<Link to={orderListUrl()} onClick={onClick}>
<Button variant="primary">
<FormattedMessage defaultMessage="Go to orders" id="kv3FWU" description="btn label" />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import React from "react";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";

export const WelcomePageWebhooksButton = () => {
import { PrimaryActionProps } from "./type";

export const WelcomePageWebhooksButton = ({ onClick }: PrimaryActionProps) => {
return (
<Link to={CustomAppSections.appsSection}>
<Link to={CustomAppSections.appsSection} onClick={onClick}>
<Button variant="primary">
<FormattedMessage defaultMessage="Go to Webhooks" id="5TzisG" description="btn label" />
</Button>
Expand Down
3 changes: 3 additions & 0 deletions src/welcomePage/WelcomePageOnboarding/components/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface PrimaryActionProps {
onClick: () => void;
}
26 changes: 19 additions & 7 deletions src/welcomePage/WelcomePageOnboarding/hooks/useOnboardingData.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useAnalytics } from "@dashboard/components/ProductAnalytics/useAnalytics";
import { Button } from "@saleor/macaw-ui-next";
import React, { ReactNode } from "react";
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
Expand All @@ -22,10 +23,12 @@ const getStepsData = ({
intl,
isStepCompleted,
onStepComplete,
trackOnboardingEvent,
}: {
intl: IntlShape;
isStepCompleted: (step: OnboardingStepsIDs) => boolean;
onStepComplete: (step: OnboardingStepsIDs) => void;
trackOnboardingEvent: (event: OnboardingStepsIDs) => void;
}): OnboardingStepData[] => [
{
id: "get-started",
Expand All @@ -44,8 +47,11 @@ const getStepsData = ({
actions: !isStepCompleted("get-started") ? (
<Button
variant="primary"
onClick={() => onStepComplete("get-started")}
data-test-id="get-started-next-step-btn"
onClick={() => {
onStepComplete("get-started");
trackOnboardingEvent("get-started");
}}
>
<FormattedMessage defaultMessage="Next step" id="d+qgix" />
</Button>
Expand All @@ -67,7 +73,7 @@ const getStepsData = ({
isCompleted: isStepCompleted("create-product"),
actions: (
<>
<WelcomePageCreateProductButton />
<WelcomePageCreateProductButton onClick={() => trackOnboardingEvent("create-product")} />
{!isStepCompleted("create-product") && (
<Button
variant="secondary"
Expand Down Expand Up @@ -96,7 +102,7 @@ const getStepsData = ({
isCompleted: isStepCompleted("explore-orders"),
actions: (
<>
<WelcomePageOrdersButton />
<WelcomePageOrdersButton onClick={() => trackOnboardingEvent("explore-orders")} />
{!isStepCompleted("explore-orders") && (
<Button
variant="secondary"
Expand Down Expand Up @@ -125,7 +131,7 @@ const getStepsData = ({
isCompleted: isStepCompleted("graphql-playground"),
actions: (
<>
<WelcomePageCheckGraphQLButton />
<WelcomePageCheckGraphQLButton onClick={() => trackOnboardingEvent("graphql-playground")} />
{!isStepCompleted("graphql-playground") && (
<Button
variant="secondary"
Expand Down Expand Up @@ -154,7 +160,7 @@ const getStepsData = ({
isCompleted: isStepCompleted("view-webhooks"),
actions: (
<>
<WelcomePageWebhooksButton />
<WelcomePageWebhooksButton onClick={() => trackOnboardingEvent("view-webhooks")} />
{!isStepCompleted("view-webhooks") && (
<Button
variant="secondary"
Expand Down Expand Up @@ -183,7 +189,7 @@ const getStepsData = ({
isCompleted: isStepCompleted("invite-staff"),
actions: (
<>
<WelcomePageInviteStaffButton />
<WelcomePageInviteStaffButton onClick={() => trackOnboardingEvent("invite-staff")} />
{!isStepCompleted("invite-staff") && (
<Button
variant="secondary"
Expand All @@ -200,12 +206,18 @@ const getStepsData = ({

export const useOnboardingData = () => {
const intl = useIntl();
const analytics = useAnalytics();
const { markOnboardingStepAsCompleted, onboardingState } = useOnboarding();

const steps = getStepsData({
intl,
isStepCompleted: (step: OnboardingStepsIDs) => onboardingState.stepsCompleted.includes(step),
onStepComplete: markOnboardingStepAsCompleted,
onStepComplete: (step: OnboardingStepsIDs) => {
markOnboardingStepAsCompleted(step);
analytics.trackEvent("home_onboarding_step_complete_click", { step_id: step });
},
trackOnboardingEvent: (step_id: OnboardingStepsIDs) =>
analytics.trackEvent("home_onboarding_step_click", { step_id: step_id }),
});

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useAnalytics } from "@dashboard/components/ProductAnalytics/useAnalytics";
import {
handleStateChangeAfterStepCompleted,
handleStateChangeAfterToggle,
Expand All @@ -22,6 +23,7 @@ import { useOnboardingStorage } from "./useOnboardingStorage";
const OnboardingContext = React.createContext<OnboardingContextType | null>(null);

export const OnboardingProvider = ({ children }: OnboardingProviderProps) => {
const analytics = useAnalytics();
const [onboardingState, setOnboardingState] = React.useState<OnboardingState>({
onboardingExpanded: true,
stepsCompleted: [],
Expand Down Expand Up @@ -69,6 +71,7 @@ export const OnboardingProvider = ({ children }: OnboardingProviderProps) => {
};

const markAllAsCompleted = () => {
analytics.trackEvent("home_onboarding_mark_all_steps_completed");
setOnboardingState(prevOnboardingState => ({
...prevOnboardingState,
stepsCompleted: initialOnboardingSteps.map(step => step.id),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useAnalytics } from "@dashboard/components/ProductAnalytics/useAnalytics";
import { Box } from "@saleor/macaw-ui-next";
import React from "react";
import { useIntl } from "react-intl";
Expand All @@ -7,8 +8,15 @@ import { WelcomePageInfoTile } from "./WelcomePageInfoTile";

export const WelcomePageTilesContainer = () => {
const intl = useIntl();
const analytics = useAnalytics();

const tiles = getTilesData({ intl });
const handleTileButtonClick = (tileId: string) => {
analytics.trackEvent("home_tile_click", {
tile_id: tileId,
});
};

const tiles = getTilesData({ intl, onTileButtonClick: handleTileButtonClick });

return (
<Box
Expand Down
Loading

0 comments on commit c3e02b9

Please sign in to comment.