web: use new umami events api for tracking

This commit is contained in:
Abdullah Atta
2023-01-17 12:47:51 +05:00
committed by Abdullah Atta
parent 3e71ad0b0b
commit 2da32e27ff
11 changed files with 192 additions and 62 deletions

View File

@@ -1 +0,0 @@
!function(){"use strict";var t=function(t,e,n){var a=t[e];return function(){for(var e=[],i=arguments.length;i--;)e[i]=arguments[i];return n.apply(null,e),a.apply(t,e)}},e=function(){var t=window.doNotTrack,e=window.navigator,n=window.external,a=t||e.doNotTrack||e.msDoNotTrack||n&&"function"==typeof n.msTrackingProtectionEnabled&&n.msTrackingProtectionEnabled();return!0===a||1===a||"yes"===a||"1"===a};!function(n){var a=n.screen,i=a.width,r=a.height,o=n.navigator.language,c=n.location,s=c.hostname,u=c.pathname,l=c.search,d=n.localStorage,f=n.sessionStorage,v=n.document,p=n.history,m=v.querySelector("script[data-website-id]");if(m){var h,g=function(t){return m&&m.getAttribute(t)},w=g("data-website-id"),y=g("data-host-url"),S="false"!==g("data-auto-track"),E=g("data-do-not-track"),k=g("data-cache"),b=g("data-domains"),T=d.getItem("umami.disabled")||E&&e()||b&&!b.split(",").map((function(t){return t.trim()})).includes(s),N=y?(h=y)&&h.length>1&&h.endsWith("/")?h.slice(0,-1):h:m.src.split("/").slice(0,-1).join("/"),j=i+"x"+r,q=[],I=""+u+l,L=v.referrer,O=function(t,e,n){if(!T){var a="umami.cache",i={website:n,hostname:s,screen:j,language:o,cache:k&&f.getItem(a)};e&&Object.keys(e).forEach((function(t){i[t]=e[t]})),function(t,e,n){var a=new XMLHttpRequest;a.open("POST",t,!0),a.setRequestHeader("Content-Type","application/json"),a.onreadystatechange=function(){4===a.readyState&&n&&n(a.response)},a.send(JSON.stringify(e))}(N+"/api/collect",{type:t,payload:i},(function(t){return k&&f.setItem(a,t)}))}},P=function(t,e,n){return void 0===t&&(t=I),void 0===e&&(e=L),void 0===n&&(n=w),O("pageview",{url:t,referrer:e},n)},x=function(t,e,n,a){return void 0===e&&(e="custom"),void 0===n&&(n=I),void 0===a&&(a=w),O("event",{event_type:e,event_value:t,url:n},a)},A=function(){v.querySelectorAll("[class*='umami--']").forEach((function(t){t.className.split(" ").forEach((function(e){if(/^umami--([a-z]+)--([\w]+[\w-]*)$/.test(e)){var n=e.split("--"),a=n[1],i=n[2],r=function(){return x(i,a)};q.push([t,a,r]),t.addEventListener(a,r,!0)}}))}))},H=function(){q.forEach((function(t){var e=t[0],n=t[1],a=t[2];e&&e.removeEventListener(n,a,!0)})),q.length=0},R=function(t,e,n){if(n){H(),L=I;var a=n.toString();(I="http"===a.substring(0,4)?"/"+a.split("/").splice(3).join("/"):a)!==L&&P(I,L),setTimeout(A,300)}};if(!n.umami){var _=function(t){return x(t)};_.trackView=P,_.trackEvent=x,_.addEvents=A,_.removeEvents=H,n.umami=_}S&&!T&&(p.pushState=t(p,"pushState",R),p.replaceState=t(p,"replaceState",R),P(I,L),A())}}(window)}();

View File

@@ -298,7 +298,7 @@ function CalltoAction({ action, variant, sx, removeAnnouncement }) {
onClick={async () => {
if (removeAnnouncement) removeAnnouncement();
closeOpenedDialog();
trackEvent(ANALYTICS_EVENTS.announcementCta, action.data);
trackEvent(ANALYTICS_EVENTS.announcementCta, action);
switch (action.type) {
case "link": {
const url = new URL(action.data);

View File

@@ -51,10 +51,7 @@ function Announcements({ announcements, removeAnnouncement }) {
}}
title="Dismiss announcement"
onClick={() => {
trackEvent(
ANALYTICS_EVENTS.announcementDismissed,
announcement.title
);
trackEvent(ANALYTICS_EVENTS.announcementDismissed, announcement);
removeAnnouncement && removeAnnouncement(announcement.id);
}}
>

View File

@@ -41,6 +41,7 @@ import { useCallback, useState } from "react";
import Config from "../../utils/config";
import { getAllAccents } from "@notesnook/theme";
import { isMacStoreApp } from "../../utils/platform";
import { doNotTrack } from "../../utils/telemetry";
const newUserSteps = [
{
@@ -48,7 +49,7 @@ const newUserSteps = [
subtitle: "Write with freedom. Never compromise on privacy again.",
buttonText: "Get started",
image: <Note width={120} />,
component: TrackingConsent
component: doNotTrack() ? null : TrackingConsent
},
{
title: "Choose your style",

View File

@@ -33,7 +33,8 @@ function Toggle(props) {
onToggled,
onlyIf,
premium,
testId
testId,
disabled
} = props;
const onClick = useCallback(async () => {
if (isUserPremium() || !premium || isToggled) onToggled();
@@ -45,10 +46,11 @@ function Toggle(props) {
if (onlyIf === false) return null;
return (
<Flex
onClick={onClick}
onClick={disabled ? null : onClick}
data-test-id={testId}
py={2}
sx={{
opacity: disabled ? 0.7 : 1,
cursor: "pointer",
borderBottom: "1px solid",
borderBottomColor: "border",
@@ -58,7 +60,7 @@ function Toggle(props) {
}}
>
<Tip text={title} tip={isToggled ? onTip : offTip} sx={{ mr: 2 }} />
<Switch onClick={onClick} checked={isToggled} />
<Switch onClick={disabled ? null : onClick} checked={isToggled} />
</Flex>
);
}

View File

@@ -19,10 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useEffect, useState } from "react";
import { initializeDatabase } from "../common/db";
import { loadTrackerScript } from "../utils/analytics";
import "../utils/analytics";
if (process.env.NODE_ENV === "production") {
loadTrackerScript();
console.log = () => {};
}

View File

@@ -0,0 +1,35 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
import {
doNotTrack,
isTelemetryEnabled,
setTelemetry
} from "../utils/telemetry";
export function useTelemetry() {
const [enabled, setEnabled] = useState(isTelemetryEnabled());
useEffect(() => {
setTelemetry(enabled);
}, [enabled]);
return [enabled, setEnabled, doNotTrack()] as const;
}

View File

@@ -17,42 +17,21 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Config from "./config";
import { getPlatform } from "./platform";
import { isTelemetryEnabled } from "./telemetry";
import { appVersion } from "./version";
declare global {
interface Window {
umami?: {
trackEvent: (
value: string,
type: string,
url?: string,
websiteId?: string
) => void;
trackView: (url: string, referrer?: string, websiteId?: string) => void;
};
}
}
type PageView = {
type: "pageview";
referrer: string;
url?: string;
};
export function loadTrackerScript() {
if (Config.get("telemetry") === "false") return;
const script = document.createElement("script");
script.src = "/an.js";
script.async = true;
script.dataset.websiteId = "f16c07d9-c77b-4781-bfbd-f58e95640002";
if (process.env.REACT_APP_PLATFORM !== "desktop")
script.dataset.domains = "app.notesnook.com";
script.dataset.autoTrack = "false";
script.dataset.doNotTrack = "true";
script.dataset.hostUrl = "https://analytics.streetwriters.co";
const firstScriptElement = document.getElementsByTagName("script")[0];
script.onload = function () {
trackVisit();
};
firstScriptElement.parentNode?.insertBefore(script, firstScriptElement);
}
type Event = {
type: "event";
event_name: string;
event_data: Record<string, unknown>;
};
type TrackerEvent = {
name: string;
@@ -104,22 +83,86 @@ export const ANALYTICS_EVENTS = {
}
} as const;
export function trackEvent(event: TrackerEvent, eventMessage?: string) {
if (Config.get("telemetry") === "false") return;
if (!window.umami) return;
const baseUrl = `https://analytics.streetwriters.co/api/collect`;
async function trackUmamiView(url: string, referrer?: string) {
return collect({
type: "pageview",
referrer: referrer || window.document.referrer,
url
});
}
function trackUmamiEvent(name: string, data: Record<string, unknown>) {
return collect({
type: "event",
event_name: name,
event_data: data
});
}
async function collect(event: PageView | Event) {
const {
screen: { width, height },
navigator: { language },
location: { hostname, pathname, search }
} = window;
const screen = `${width}x${height}`;
const currentUrl =
(event.type === "pageview" && event.url) || `${pathname}${search}`;
const body = {
payload: {
website: `f16c07d9-c77b-4781-bfbd-f58e95640002`,
hostname,
screen,
language,
url: currentUrl,
...event
},
type: event.type
};
try {
await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body),
keepalive: true
});
} catch (e) {
console.error(e);
}
}
export function trackEvent(
event: TrackerEvent,
data?: Record<string, unknown>
) {
if (isTelemetryEnabled()) return;
if (event.type === "view") trackVisit(event.name);
else if (eventMessage) window.umami.trackEvent(eventMessage, event.name);
else if (data) trackUmamiEvent(event.name, data);
}
export function trackVisit(url = "/") {
if (Config.get("telemetry") === "false") return;
const platform = getPlatform();
if (!window.umami || !platform) return;
if (isTelemetryEnabled()) return;
window.umami.trackView(url, window.document.referrer);
const platform = getPlatform();
if (!platform) return;
trackUmamiView(window.document.referrer);
if (url === "/")
trackEvent(
ANALYTICS_EVENTS.version,
`${appVersion.formatted}-${platform.toLowerCase()}`
);
trackEvent(ANALYTICS_EVENTS.version, {
version: appVersion.formatted,
platform
});
}
if (isTelemetryEnabled()) {
document.addEventListener("readystatechange", () => {
trackVisit();
});
}

View File

@@ -0,0 +1,50 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Config from "./config";
export function isTelemetryEnabled() {
return !doNotTrack() && Config.get("telemetry", true);
}
export function setTelemetry(state: boolean) {
Config.set("telemetry", state);
}
export function doNotTrack() {
const { navigator, external } = window;
const msTrackProtection = "msTrackingProtectionEnabled";
const msTracking = () => {
return (
external &&
msTrackProtection in external &&
typeof external[msTrackProtection] === "function" &&
external[msTrackProtection]()
);
};
const dnt =
("doNotTrack" in window && window.doNotTrack) ||
navigator.doNotTrack ||
("msDoNotTrack" in navigator && navigator.doNotTrack) ||
msTracking();
return dnt == "1" || dnt === "yes";
}

View File

@@ -156,7 +156,7 @@ function BlogPromoBanner() {
title={account.title}
onClick={() => {
window.open(account.link, "_blank");
trackEvent(ANALYTICS_EVENTS.socialLink, account.title);
trackEvent(ANALYTICS_EVENTS.socialLink, { link: account.link });
}}
size={30}
sx={{ mr: 1, cursor: "pointer" }}

View File

@@ -74,6 +74,7 @@ import { clearLogs, downloadLogs } from "../utils/logger";
import { exportNotes } from "../common/export";
import { scheduleBackups } from "../common/reminders";
import usePrivacyMode from "../hooks/use-privacy-mode";
import { useTelemetry } from "../hooks/use-telemetry";
function subscriptionStatusToString(user) {
const status = user?.subscription?.type;
@@ -188,10 +189,8 @@ function Settings() {
"backupStorageLocation",
PATHS.backupsDirectory
);
const [enableTelemetry, setEnableTelemetry] = usePersistentState(
"telemetry",
true
);
const [enableTelemetry, setEnableTelemetry, isTelemetryDisabled] =
useTelemetry();
const [privacyMode, setPrivacyMode] = usePrivacyMode();
const [showReminderNotifications, setShowReminderNotifications] =
usePersistentState("reminderNotifications", true);
@@ -824,11 +823,16 @@ function Settings() {
<Toggle
title="Enable telemetry"
onTip="Usage data & crash reports will be sent to us (no 3rd party involved) for analytics. All data is anonymous as mentioned in our privacy policy."
offTip="Do not collect any data or crash reports"
offTip={
isTelemetryDisabled
? "All telemetry is disabled because of Do Not Track being enabled in your browser."
: "Do not collect any data or crash reports"
}
onToggled={() => {
setEnableTelemetry(!enableTelemetry);
}}
isToggled={enableTelemetry}
disabled={isTelemetryDisabled}
/>
<Button variant="list" onClick={showTrackingDetailsDialog}>
<Tip