Skip to content

Commit 115669f

Browse files
feat: working matomo analytics (test environment)
1 parent 7ce00c4 commit 115669f

7 files changed

Lines changed: 313 additions & 0 deletions

File tree

src/argo-archive-list.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import filingDrawer from "assets/images/filing-drawer.avif";
1212

1313
import { getLocalOption } from "./localstorage";
1414
import { Index as FlexIndex } from "flexsearch";
15+
import { onPageClicked } from "./events";
1516

1617
@customElement("argo-archive-list")
1718
export class ArgoArchiveList extends LitElement {
@@ -434,6 +435,7 @@ export class ArgoArchiveList extends LitElement {
434435
}
435436

436437
private async _openPage(page: { ts: string; url: string }) {
438+
onPageClicked(page.url);
437439
const tsParam = new Date(Number(page.ts))
438440
.toISOString()
439441
.replace(/[-:TZ.]/g, "");

src/events.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { trackEvent } from "./matomo";
2+
import { getLocalOption } from "./localstorage";
3+
4+
// Track when a user clicks on an archived page to view it
5+
export async function onPageClicked(pageUrl: string): Promise<void> {
6+
console.log("onPageClicked called with URL:", pageUrl);
7+
await trackEvent("Archive", "ViewPage", pageUrl);
8+
}
9+
10+
// Track when a torrent is created for sharing
11+
export async function onTorrentCreated(numPages: number): Promise<void> {
12+
console.log("onTorrentCreated called with pages:", numPages);
13+
await trackEvent("Sharing", "TorrentCreated", `${numPages} pages`);
14+
}
15+
16+
// Track when a page is successfully archived
17+
export async function onPageArchived(
18+
pageUrl: string,
19+
pageSize?: number,
20+
): Promise<void> {
21+
console.log("onPageArchived called:", pageUrl, pageSize);
22+
await trackEvent("Archive", "PageArchived", pageUrl);
23+
24+
// If page size is provided, track it separately
25+
if (pageSize !== undefined) {
26+
await trackEvent("Archive", "PageSize", `${Math.round(pageSize / 1024)}KB`);
27+
}
28+
}
29+
30+
// Track settings changes
31+
export async function onSettingsChanged(
32+
settingName: string,
33+
value: string | boolean | number,
34+
): Promise<void> {
35+
console.log("onSettingsChanged:", settingName, value);
36+
await trackEvent("Settings", settingName, String(value));
37+
}
38+
39+
// Track total archive size
40+
export async function trackArchiveSize(totalSizeBytes: number): Promise<void> {
41+
const sizeMB = Math.round(totalSizeBytes / (1024 * 1024));
42+
console.log("trackArchiveSize:", sizeMB, "MB");
43+
await trackEvent("Archive", "TotalSize", `${sizeMB}MB`);
44+
}
45+
46+
// Track when archiving starts
47+
export async function onArchivingStarted(pageUrl: string): Promise<void> {
48+
console.log("onArchivingStarted:", pageUrl);
49+
await trackEvent("Archive", "Started", pageUrl);
50+
}
51+
52+
// Track when archiving stops
53+
export async function onArchivingStopped(
54+
reason: string = "manual",
55+
): Promise<void> {
56+
console.log("onArchivingStopped:", reason);
57+
await trackEvent("Archive", "Stopped", reason);
58+
}

src/ext/bg.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import {
1010
getSharedArchives,
1111
} from "../localstorage";
1212
import { isValidUrl } from "../utils";
13+
14+
import {
15+
trackArchiveSize,
16+
onArchivingStarted,
17+
onArchivingStopped,
18+
} from "../events";
1319
// ===========================================================================
1420
self.recorders = {};
1521
self.newRecId = null;
@@ -24,6 +30,7 @@ let defaultCollId = null;
2430
let autorun = false;
2531
let isRecordingEnabled = false;
2632
let skipDomains = [] as string[];
33+
let lastTrackedTotalSize = 0;
2734

2835
const openWinMap = new Map();
2936

@@ -39,6 +46,31 @@ let sidepanelPort = null;
3946
skipDomains = (await getLocalOption("skipDomains")) || [];
4047
})();
4148

49+
async function checkAndTrackArchiveSize() {
50+
try {
51+
const collId = await getLocalOption("defaultCollId");
52+
if (!collId) return;
53+
54+
const coll = await collLoader.loadColl(collId);
55+
if (!coll?.store?.getAllPages) return;
56+
57+
const pages = await coll.store.getAllPages();
58+
// @ts-expect-error any
59+
const totalSize = pages.reduce((sum, page) => sum + (page.size || 0), 0);
60+
61+
const sizeDiff = totalSize - lastTrackedTotalSize;
62+
if (
63+
sizeDiff > 10 * 1024 * 1024 ||
64+
(totalSize > 0 && lastTrackedTotalSize === 0)
65+
) {
66+
await trackArchiveSize(totalSize);
67+
lastTrackedTotalSize = totalSize;
68+
}
69+
} catch (error) {
70+
console.error("Error tracking archive size:", error);
71+
}
72+
}
73+
4274
// ===========================================================================
4375

4476
function main() {
@@ -110,6 +142,7 @@ function sidepanelHandler(port) {
110142
if (coll?.store?.getAllPages) {
111143
const pages = await coll.store.getAllPages();
112144
port.postMessage({ type: "pages", pages });
145+
await checkAndTrackArchiveSize();
113146
} else {
114147
port.postMessage({ type: "pages", pages: [] });
115148
}
@@ -127,6 +160,7 @@ function sidepanelHandler(port) {
127160
await coll.store.deletePage(id);
128161
}
129162

163+
await checkAndTrackArchiveSize();
130164
// now re-send the new list of pages
131165
const pages = await coll.store.getAllPages();
132166
port.postMessage({ type: "pages", pages });
@@ -156,6 +190,10 @@ function sidepanelHandler(port) {
156190
//@ts-expect-error - 2 parameters but 3
157191
tab.url,
158192
);
193+
194+
if (tab.url) {
195+
await onArchivingStarted(tab.url);
196+
}
159197
}
160198

161199
port.postMessage({
@@ -179,6 +217,9 @@ function sidepanelHandler(port) {
179217
stopRecorder(tabId);
180218
}
181219

220+
await checkAndTrackArchiveSize();
221+
await onArchivingStopped("manual");
222+
182223
port.postMessage({
183224
type: "status",
184225
recording: false,
@@ -223,6 +264,21 @@ chrome.runtime.onMessage.addListener(
223264
// @ts-expect-error - TS7006 - Parameter 'message' implicitly has an 'any' type.
224265
(message /*sender, sendResponse*/) => {
225266
console.log("onMessage", message);
267+
268+
if (message.type === "matomoTrack" && message.url) {
269+
fetch(message.url, {
270+
method: "GET",
271+
mode: "no-cors",
272+
})
273+
.then(() => {
274+
console.log("Matomo tracking sent from background:", message.url);
275+
})
276+
.catch((error) => {
277+
console.error("Matomo tracking error in background:", error);
278+
});
279+
return true;
280+
}
281+
226282
switch (message.msg) {
227283
case "optionsChanged":
228284
for (const rec of Object.values(self.recorders)) {
@@ -244,6 +300,11 @@ chrome.runtime.onMessage.addListener(
244300
case "disableCSP":
245301
disableCSPForTab(message.tabId);
246302
break;
303+
304+
case "checkArchiveSize":
305+
// Check and track archive size when requested
306+
checkAndTrackArchiveSize();
307+
break;
247308
}
248309
return true;
249310
},
@@ -531,6 +592,19 @@ async function disableCSPForTab(tabId) {
531592
// ===========================================================================
532593
chrome.runtime.onInstalled.addListener(main);
533594

595+
chrome.runtime.onStartup.addListener(async () => {
596+
await checkAndTrackArchiveSize();
597+
});
598+
599+
setInterval(
600+
async () => {
601+
if (Object.keys(self.recorders).length > 0) {
602+
await checkAndTrackArchiveSize();
603+
}
604+
},
605+
5 * 60 * 1000,
606+
);
607+
534608
if (self.importScripts) {
535609
self.importScripts("sw.js");
536610
}

src/matomo.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// matomo.ts - Matomo tracking with opt-out and persistent user ID
2+
3+
import { getLocalOption, setLocalOption } from "./localstorage";
4+
5+
const MATOMO_URL = "https://argo-chrome-test.matomo.cloud/matomo.php";
6+
const SITE_ID = "2";
7+
const USER_ID_KEY = "matomoUserId";
8+
9+
/**
10+
* Ensure there is a persistent user ID in local storage.
11+
* If one doesn't exist, generate a random hex string, store it, and return it.
12+
*/
13+
async function getOrCreateUserId(): Promise<string> {
14+
let stored = await getLocalOption(USER_ID_KEY);
15+
if (stored && typeof stored === "string") {
16+
return stored;
17+
}
18+
19+
// Generate a 16-byte (128-bit) hex string
20+
const randomId = Array.from({ length: 16 })
21+
.map(() =>
22+
Math.floor(Math.random() * 256)
23+
.toString(16)
24+
.padStart(2, "0"),
25+
)
26+
.join("");
27+
28+
await setLocalOption(USER_ID_KEY, randomId);
29+
return randomId;
30+
}
31+
32+
/**
33+
* Reads the "analyticsEnabled" key via getLocalOption.
34+
* We expect it to be stored as "1" or "0".
35+
* Returns true only if the stored value is exactly "1".
36+
*/
37+
async function checkAnalyticsEnabled(): Promise<boolean> {
38+
const stored = await getLocalOption("analyticsEnabled");
39+
return stored === "1";
40+
}
41+
42+
/**
43+
* Check if we're in the background/service worker context
44+
*/
45+
function isBackgroundContext(): boolean {
46+
// Check if we have access to chrome.tabs (only available in background)
47+
return typeof chrome !== "undefined" && chrome.tabs !== undefined;
48+
}
49+
50+
/**
51+
* Send a simple event to Matomo, but only if analyticsEnabled === "1".
52+
* Includes a persistent user ID (uid) in every request.
53+
*/
54+
export async function trackEvent(
55+
category: string,
56+
action: string,
57+
name?: string,
58+
): Promise<void> {
59+
try {
60+
const isEnabled = await checkAnalyticsEnabled();
61+
if (!isEnabled) {
62+
console.log("Matomo tracking is disabled; skipping event:", {
63+
category,
64+
action,
65+
name,
66+
});
67+
return;
68+
}
69+
70+
const userId = await getOrCreateUserId();
71+
const params = new URLSearchParams({
72+
// Required
73+
idsite: SITE_ID,
74+
rec: "1",
75+
76+
// Event parameters
77+
e_c: category,
78+
e_a: action,
79+
e_n: name || "",
80+
81+
// Basic info
82+
url: "chrome-extension://" + chrome.runtime.id,
83+
_id: Math.random().toString(16).substr(2, 16),
84+
rand: Date.now().toString(),
85+
apiv: "1",
86+
87+
// Don't return image
88+
send_image: "0",
89+
90+
// Persistent user ID
91+
uid: userId,
92+
});
93+
94+
const url = `${MATOMO_URL}?${params.toString()}`;
95+
console.log("Sending Matomo event:", {
96+
category,
97+
action,
98+
name,
99+
userId,
100+
url,
101+
});
102+
103+
// If we're in the background context, use fetch directly
104+
if (isBackgroundContext()) {
105+
await fetch(url, {
106+
method: "GET",
107+
mode: "no-cors",
108+
});
109+
console.log("Matomo event sent directly from background");
110+
} else {
111+
// Otherwise, try to send via message to background
112+
try {
113+
await chrome.runtime.sendMessage({
114+
type: "matomoTrack",
115+
url: url,
116+
});
117+
console.log("Matomo event sent via message");
118+
} catch (error) {
119+
// Fallback to image beacon if messaging fails
120+
const img = new Image();
121+
img.src = url;
122+
console.log("Matomo event sent via image beacon");
123+
}
124+
}
125+
126+
console.log("Matomo event sent successfully");
127+
} catch (error) {
128+
console.error("Matomo tracking error:", error);
129+
}
130+
}

src/recorder.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
} from "./consts";
2323
import { getLocalOption } from "./localstorage";
2424

25+
import { onPageArchived } from "./events";
26+
2527
const encoder = new TextEncoder();
2628

2729
const MAX_CONCURRENT_FETCH = 6;
@@ -1136,6 +1138,15 @@ class Recorder {
11361138
// @ts-expect-error - TS2339 - Property '_cachePageInfo' does not exist on type 'Recorder'.
11371139
this._cachePageInfo = null;
11381140
}
1141+
1142+
if (finished && currPage.url) {
1143+
onPageArchived(currPage.url, currPage.size)
1144+
.then(() => {})
1145+
.catch((err) => {
1146+
console.error("onPageArchived failed:", err);
1147+
});
1148+
}
1149+
11391150
return res;
11401151
}
11411152

0 commit comments

Comments
 (0)