Matomo integration (#7009)

This commit is contained in:
Tom Moor
2024-06-12 08:03:38 -04:00
committed by GitHub
parent 0bf66cc560
commit df3cd22aee
19 changed files with 314 additions and 62 deletions

View File

@@ -2,13 +2,14 @@
/* global ga */
import escape from "lodash/escape";
import * as React from "react";
import { IntegrationService } from "@shared/types";
import { IntegrationService, PublicEnv } from "@shared/types";
import env from "~/env";
type Props = {
children?: React.ReactNode;
};
// TODO: Refactor this component to allow injection from plugins
const Analytics: React.FC = ({ children }: Props) => {
// Google Analytics 3
React.useEffect(() => {
@@ -43,12 +44,16 @@ const Analytics: React.FC = ({ children }: Props) => {
React.useEffect(() => {
const measurementIds = [];
if (env.analytics.service === IntegrationService.GoogleAnalytics) {
measurementIds.push(escape(env.analytics.settings?.measurementId));
}
if (env.GOOGLE_ANALYTICS_ID?.startsWith("G-")) {
measurementIds.push(env.GOOGLE_ANALYTICS_ID);
}
(env.analytics as PublicEnv["analytics"]).forEach((integration) => {
if (integration.service === IntegrationService.GoogleAnalytics) {
measurementIds.push(escape(integration.settings?.measurementId));
}
});
if (measurementIds.length === 0) {
return;
}
@@ -75,6 +80,32 @@ const Analytics: React.FC = ({ children }: Props) => {
document.getElementsByTagName("head")[0]?.appendChild(script);
}, []);
// Matomo
React.useEffect(() => {
(env.analytics as PublicEnv["analytics"]).forEach((integration) => {
if (integration.service !== IntegrationService.Matomo) {
return;
}
// @ts-expect-error - Matomo global variable
const _paq = (window._paq = window._paq || []);
_paq.push(["trackPageView"]);
_paq.push(["enableLinkTracking"]);
(function () {
const u = integration.settings?.instanceUrl;
_paq.push(["setTrackerUrl", u + "matomo.php"]);
_paq.push(["setSiteId", integration.settings?.measurementId]);
const d = document,
g = d.createElement("script"),
s = d.getElementsByTagName("script")[0];
g.type = "text/javascript";
g.async = true;
g.src = u + "matomo.js";
s.parentNode?.insertBefore(g, s);
})();
});
}, []);
return <>{children}</>;
};

View File

@@ -1,3 +1,4 @@
import sortBy from "lodash/sortBy";
import {
EmailIcon,
ProfileIcon,
@@ -18,7 +19,6 @@ import {
import React, { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
import GoogleIcon from "~/components/Icons/GoogleIcon";
import ZapierIcon from "~/components/Icons/ZapierIcon";
import PluginLoader from "~/utils/PluginLoader";
import isCloudHosted from "~/utils/isCloudHosted";
@@ -32,7 +32,6 @@ const ApiKeys = lazy(() => import("~/scenes/Settings/ApiKeys"));
const Details = lazy(() => import("~/scenes/Settings/Details"));
const Export = lazy(() => import("~/scenes/Settings/Export"));
const Features = lazy(() => import("~/scenes/Settings/Features"));
const GoogleAnalytics = lazy(() => import("~/scenes/Settings/GoogleAnalytics"));
const Groups = lazy(() => import("~/scenes/Settings/Groups"));
const Import = lazy(() => import("~/scenes/Settings/Import"));
const Members = lazy(() => import("~/scenes/Settings/Members"));
@@ -177,14 +176,6 @@ const useSettingsConfig = () => {
group: t("Integrations"),
icon: BuildingBlocksIcon,
},
{
name: t("Google Analytics"),
path: integrationSettingsPath("google-analytics"),
component: GoogleAnalytics,
enabled: can.update,
group: t("Integrations"),
icon: GoogleIcon,
},
{
name: "Zapier",
path: integrationSettingsPath("zapier"),
@@ -196,30 +187,37 @@ const useSettingsConfig = () => {
];
// Plugins
Object.values(PluginLoader.plugins).map((plugin) => {
const hasSettings = !!plugin.settings;
const enabledInDeployment =
!plugin.config?.deployments ||
plugin.config.deployments.length === 0 ||
(plugin.config.deployments.includes("cloud") && isCloudHosted) ||
(plugin.config.deployments.includes("enterprise") && !isCloudHosted);
const insertIndex = items.findIndex((i) => i.group === t("Integrations"));
items.splice(
insertIndex,
0,
...(sortBy(
Object.values(PluginLoader.plugins),
(plugin) => plugin.config?.priority ?? 0
).map((plugin) => {
const hasSettings = !!plugin.settings;
const enabledInDeployment =
!plugin.config?.deployments ||
plugin.config.deployments.length === 0 ||
(plugin.config.deployments.includes("community") && !isCloudHosted) ||
(plugin.config.deployments.includes("cloud") && isCloudHosted) ||
(plugin.config.deployments.includes("enterprise") && !isCloudHosted);
const item = {
name: t(plugin.config.name),
path: integrationSettingsPath(plugin.id),
// TODO: Remove hardcoding of plugin id here
group: plugin.id === "collections" ? t("Workspace") : t("Integrations"),
component: plugin.settings,
enabled:
enabledInDeployment &&
hasSettings &&
(plugin.config.roles?.includes(user.role) || can.update),
icon: plugin.icon,
} as ConfigItem;
const insertIndex = items.findIndex((i) => i.group === t("Integrations"));
items.splice(insertIndex, 0, item);
});
return {
name: t(plugin.config.name),
path: integrationSettingsPath(plugin.id),
// TODO: Remove hardcoding of plugin id here
group:
plugin.id === "collections" ? t("Workspace") : t("Integrations"),
component: plugin.settings,
enabled:
enabledInDeployment &&
hasSettings &&
(plugin.config.roles?.includes(user.role) || can.update),
icon: plugin.icon,
};
}) as ConfigItem[])
);
return items;
}, [t, can.createApiKey, can.update, can.createImport, can.createExport]);

View File

@@ -1,107 +0,0 @@
import find from "lodash/find";
import { observer } from "mobx-react";
import * as React from "react";
import { useForm } from "react-hook-form";
import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import { IntegrationType, IntegrationService } from "@shared/types";
import Integration from "~/models/Integration";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import GoogleIcon from "~/components/Icons/GoogleIcon";
import Input from "~/components/Input";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import SettingRow from "./components/SettingRow";
type FormData = {
measurementId: string;
};
function GoogleAnalytics() {
const { integrations } = useStores();
const { t } = useTranslation();
const integration = find(integrations.orderedData, {
type: IntegrationType.Analytics,
service: IntegrationService.GoogleAnalytics,
}) as Integration<IntegrationType.Analytics> | undefined;
const {
register,
reset,
handleSubmit: formHandleSubmit,
formState,
} = useForm<FormData>({
mode: "all",
defaultValues: {
measurementId: integration?.settings.measurementId,
},
});
React.useEffect(() => {
void integrations.fetchPage({
type: IntegrationType.Analytics,
});
}, [integrations]);
React.useEffect(() => {
reset({ measurementId: integration?.settings.measurementId });
}, [integration, reset]);
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
if (data.measurementId) {
await integrations.save({
id: integration?.id,
type: IntegrationType.Analytics,
service: IntegrationService.GoogleAnalytics,
settings: {
measurementId: data.measurementId,
},
});
} else {
await integration?.delete();
}
toast.success(t("Settings saved"));
} catch (err) {
toast.error(err.message);
}
},
[integrations, integration, t]
);
return (
<Scene title={t("Google Analytics")} icon={<GoogleIcon />}>
<Heading>{t("Google Analytics")}</Heading>
<Text as="p" type="secondary">
<Trans>
Add a Google Analytics 4 measurement ID to send document views and
analytics from the workspace to your own Google Analytics account.
</Trans>
</Text>
<form onSubmit={formHandleSubmit(handleSubmit)}>
<SettingRow
label={t("Measurement ID")}
name="measurementId"
description={t(
'Create a "Web" stream in your Google Analytics admin dashboard and copy the measurement ID from the generated code snippet to install.'
)}
border={false}
>
<Input placeholder="G-XXXXXXXXX1" {...register("measurementId")} />
</SettingRow>
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? `${t("Saving")}` : t("Save")}
</Button>
</form>
</Scene>
);
}
export default observer(GoogleAnalytics);

View File

@@ -8,6 +8,7 @@ interface Plugin {
description: string;
roles?: UserRole[];
deployments?: string[];
priority?: number;
};
settings: React.FC;
icon: React.FC<{ size?: number; fill?: string }>;