diff --git a/apps/web/public/an.js b/apps/web/public/an.js deleted file mode 100644 index 8a6b8c5e9..000000000 --- a/apps/web/public/an.js +++ /dev/null @@ -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)}(); \ No newline at end of file diff --git a/apps/web/src/components/announcements/body.js b/apps/web/src/components/announcements/body.js index f313b15bd..dc2fb2a8b 100644 --- a/apps/web/src/components/announcements/body.js +++ b/apps/web/src/components/announcements/body.js @@ -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); diff --git a/apps/web/src/components/announcements/index.js b/apps/web/src/components/announcements/index.js index 150aef2e3..d6a6a8a2c 100644 --- a/apps/web/src/components/announcements/index.js +++ b/apps/web/src/components/announcements/index.js @@ -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); }} > diff --git a/apps/web/src/components/dialogs/onboarding-dialog.js b/apps/web/src/components/dialogs/onboarding-dialog.js index fb5498971..1742e056b 100644 --- a/apps/web/src/components/dialogs/onboarding-dialog.js +++ b/apps/web/src/components/dialogs/onboarding-dialog.js @@ -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: , - component: TrackingConsent + component: doNotTrack() ? null : TrackingConsent }, { title: "Choose your style", diff --git a/apps/web/src/components/toggle/index.js b/apps/web/src/components/toggle/index.js index 2f241b88b..751a7d647 100644 --- a/apps/web/src/components/toggle/index.js +++ b/apps/web/src/components/toggle/index.js @@ -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 ( - + ); } diff --git a/apps/web/src/hooks/use-database.js b/apps/web/src/hooks/use-database.js index a0616d6a4..e555c9327 100644 --- a/apps/web/src/hooks/use-database.js +++ b/apps/web/src/hooks/use-database.js @@ -19,10 +19,9 @@ along with this program. If not, see . 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 = () => {}; } diff --git a/apps/web/src/hooks/use-telemetry.ts b/apps/web/src/hooks/use-telemetry.ts new file mode 100644 index 000000000..9a2ef2fd1 --- /dev/null +++ b/apps/web/src/hooks/use-telemetry.ts @@ -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 . +*/ + +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; +} diff --git a/apps/web/src/utils/analytics.ts b/apps/web/src/utils/analytics.ts index 0ebc102c7..3d4fdbdc2 100644 --- a/apps/web/src/utils/analytics.ts +++ b/apps/web/src/utils/analytics.ts @@ -17,42 +17,21 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -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; +}; 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) { + 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 +) { + 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(); + }); } diff --git a/apps/web/src/utils/telemetry.ts b/apps/web/src/utils/telemetry.ts new file mode 100644 index 000000000..b2b766d79 --- /dev/null +++ b/apps/web/src/utils/telemetry.ts @@ -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 . +*/ + +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"; +} diff --git a/apps/web/src/views/email-confirmed.js b/apps/web/src/views/email-confirmed.js index 289059ead..d5880c9a9 100644 --- a/apps/web/src/views/email-confirmed.js +++ b/apps/web/src/views/email-confirmed.js @@ -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" }} diff --git a/apps/web/src/views/settings.js b/apps/web/src/views/settings.js index 8ec586a01..ad5491bd9 100644 --- a/apps/web/src/views/settings.js +++ b/apps/web/src/views/settings.js @@ -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() { { setEnableTelemetry(!enableTelemetry); }} isToggled={enableTelemetry} + disabled={isTelemetryDisabled} />