mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
feat: implement analytics tracking for desktop app events
- Added event tracking for desktop app activation and quitting. - Introduced analytics bridge in preload script to handle user identification and event capturing. - Updated IPC channels to support analytics-related actions. - Enhanced analytics functionality in the main process to track user interactions and application updates. - Integrated analytics tracking for folder watching and deep link handling. - Improved connector setup tracking in the web application. This commit enhances the overall analytics capabilities of the application, ensuring better user behavior insights and event tracking across both desktop and web environments.
This commit is contained in:
parent
b38a297349
commit
b440610e04
18 changed files with 673 additions and 80 deletions
|
|
@ -3,14 +3,27 @@ import { machineIdSync } from 'node-machine-id';
|
|||
import { app } from 'electron';
|
||||
|
||||
let client: PostHog | null = null;
|
||||
let distinctId = '';
|
||||
let machineId = '';
|
||||
let currentDistinctId = '';
|
||||
let identifiedUserId: string | null = null;
|
||||
|
||||
function baseProperties(): Record<string, unknown> {
|
||||
return {
|
||||
platform: 'desktop',
|
||||
app_version: app.getVersion(),
|
||||
os: process.platform,
|
||||
arch: process.arch,
|
||||
machine_id: machineId,
|
||||
};
|
||||
}
|
||||
|
||||
export function initAnalytics(): void {
|
||||
const key = process.env.POSTHOG_KEY;
|
||||
if (!key) return;
|
||||
|
||||
try {
|
||||
distinctId = machineIdSync(true);
|
||||
machineId = machineIdSync(true);
|
||||
currentDistinctId = machineId;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
|
@ -22,17 +35,92 @@ export function initAnalytics(): void {
|
|||
});
|
||||
}
|
||||
|
||||
export function trackEvent(event: string, properties?: Record<string, unknown>): void {
|
||||
export function getMachineId(): string {
|
||||
return machineId;
|
||||
}
|
||||
|
||||
export function getDistinctId(): string {
|
||||
return currentDistinctId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify the current logged-in user in PostHog so main-process desktop
|
||||
* events (and linked anonymous machine events) are attributed to that person.
|
||||
*
|
||||
* Idempotent: calling identify repeatedly with the same userId is a no-op.
|
||||
*/
|
||||
export function identifyUser(
|
||||
userId: string,
|
||||
properties?: Record<string, unknown>
|
||||
): void {
|
||||
if (!client || !userId) return;
|
||||
if (identifiedUserId === userId) {
|
||||
// Already identified — only refresh person properties
|
||||
try {
|
||||
client.identify({
|
||||
distinctId: userId,
|
||||
properties: {
|
||||
...baseProperties(),
|
||||
$set: {
|
||||
...(properties || {}),
|
||||
platform: 'desktop',
|
||||
last_seen_at: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Link the anonymous machine distinct ID to the authenticated user
|
||||
client.identify({
|
||||
distinctId: userId,
|
||||
properties: {
|
||||
...baseProperties(),
|
||||
$anon_distinct_id: machineId,
|
||||
$set: {
|
||||
...(properties || {}),
|
||||
platform: 'desktop',
|
||||
last_seen_at: new Date().toISOString(),
|
||||
},
|
||||
$set_once: {
|
||||
first_seen_platform: 'desktop',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
identifiedUserId = userId;
|
||||
currentDistinctId = userId;
|
||||
} catch {
|
||||
// Analytics must never break the app
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user identity on logout. Subsequent events are captured anonymously
|
||||
* against the machine ID until the user logs in again.
|
||||
*/
|
||||
export function resetUser(): void {
|
||||
if (!client) return;
|
||||
identifiedUserId = null;
|
||||
currentDistinctId = machineId;
|
||||
}
|
||||
|
||||
export function trackEvent(
|
||||
event: string,
|
||||
properties?: Record<string, unknown>
|
||||
): void {
|
||||
if (!client) return;
|
||||
|
||||
try {
|
||||
client.capture({
|
||||
distinctId,
|
||||
distinctId: currentDistinctId || machineId,
|
||||
event,
|
||||
properties: {
|
||||
platform: 'desktop',
|
||||
app_version: app.getVersion(),
|
||||
os: process.platform,
|
||||
...baseProperties(),
|
||||
...properties,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { app, dialog } from 'electron';
|
||||
import { trackEvent } from './analytics';
|
||||
|
||||
const SEMVER_RE = /^\d+\.\d+\.\d+/;
|
||||
|
||||
|
|
@ -17,10 +18,18 @@ export function setupAutoUpdater(): void {
|
|||
|
||||
autoUpdater.on('update-available', (info: { version: string }) => {
|
||||
console.log(`Update available: ${info.version}`);
|
||||
trackEvent('desktop_update_available', {
|
||||
current_version: version,
|
||||
new_version: info.version,
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info: { version: string }) => {
|
||||
console.log(`Update downloaded: ${info.version}`);
|
||||
trackEvent('desktop_update_downloaded', {
|
||||
current_version: version,
|
||||
new_version: info.version,
|
||||
});
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
buttons: ['Restart', 'Later'],
|
||||
|
|
@ -29,13 +38,19 @@ export function setupAutoUpdater(): void {
|
|||
message: `Version ${info.version} has been downloaded. Restart to apply the update.`,
|
||||
}).then(({ response }: { response: number }) => {
|
||||
if (response === 0) {
|
||||
trackEvent('desktop_update_install_accepted', { new_version: info.version });
|
||||
autoUpdater.quitAndInstall();
|
||||
} else {
|
||||
trackEvent('desktop_update_install_deferred', { new_version: info.version });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (err: Error) => {
|
||||
console.log('Auto-updater: update check skipped —', err.message?.split('\n')[0]);
|
||||
trackEvent('desktop_update_error', {
|
||||
message: err.message?.split('\n')[0],
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.checkForUpdates().catch(() => {});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { app } from 'electron';
|
|||
import path from 'path';
|
||||
import { getMainWindow } from './window';
|
||||
import { getServerPort } from './server';
|
||||
import { trackEvent } from './analytics';
|
||||
|
||||
const PROTOCOL = 'surfsense';
|
||||
|
||||
|
|
@ -16,6 +17,10 @@ function handleDeepLink(url: string) {
|
|||
if (!win) return;
|
||||
|
||||
const parsed = new URL(url);
|
||||
trackEvent('desktop_deep_link_received', {
|
||||
host: parsed.hostname,
|
||||
path: parsed.pathname,
|
||||
});
|
||||
if (parsed.hostname === 'auth' && parsed.pathname === '/callback') {
|
||||
const params = parsed.searchParams.toString();
|
||||
win.loadURL(`http://localhost:${getServerPort()}/auth/callback?${params}`);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { randomUUID } from 'crypto';
|
|||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { IPC_CHANNELS } from '../ipc/channels';
|
||||
import { trackEvent } from './analytics';
|
||||
|
||||
export interface WatchedFolderConfig {
|
||||
path: string;
|
||||
|
|
@ -401,6 +402,15 @@ export async function addWatchedFolder(
|
|||
await startWatcher(config);
|
||||
}
|
||||
|
||||
trackEvent('desktop_folder_watch_added', {
|
||||
search_space_id: config.searchSpaceId,
|
||||
root_folder_id: config.rootFolderId,
|
||||
active: config.active,
|
||||
has_exclude_patterns: (config.excludePatterns?.length ?? 0) > 0,
|
||||
has_extension_filter: !!config.fileExtensions && config.fileExtensions.length > 0,
|
||||
is_update: existing >= 0,
|
||||
});
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
|
|
@ -409,6 +419,7 @@ export async function removeWatchedFolder(
|
|||
): Promise<WatchedFolderConfig[]> {
|
||||
const s = await getStore();
|
||||
const folders: WatchedFolderConfig[] = s.get(STORE_KEY, []);
|
||||
const removed = folders.find((f: WatchedFolderConfig) => f.path === folderPath);
|
||||
const updated = folders.filter((f: WatchedFolderConfig) => f.path !== folderPath);
|
||||
s.set(STORE_KEY, updated);
|
||||
|
||||
|
|
@ -418,6 +429,13 @@ export async function removeWatchedFolder(
|
|||
const ms = await getMtimeStore();
|
||||
ms.delete(folderPath);
|
||||
|
||||
if (removed) {
|
||||
trackEvent('desktop_folder_watch_removed', {
|
||||
search_space_id: removed.searchSpaceId,
|
||||
root_folder_id: removed.rootFolderId,
|
||||
});
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { app, globalShortcut, Menu, nativeImage, Tray } from 'electron';
|
|||
import path from 'path';
|
||||
import { getMainWindow, createMainWindow } from './window';
|
||||
import { getShortcuts } from './shortcuts';
|
||||
import { trackEvent } from './analytics';
|
||||
|
||||
let tray: Tray | null = null;
|
||||
let currentShortcut: string | null = null;
|
||||
|
|
@ -15,14 +16,16 @@ function getTrayIcon(): nativeImage {
|
|||
return img.resize({ width: 16, height: 16 });
|
||||
}
|
||||
|
||||
function showMainWindow(): void {
|
||||
let win = getMainWindow();
|
||||
if (!win || win.isDestroyed()) {
|
||||
win = createMainWindow('/dashboard');
|
||||
function showMainWindow(source: 'tray_click' | 'tray_menu' | 'shortcut' = 'tray_click'): void {
|
||||
const existing = getMainWindow();
|
||||
const reopened = !existing || existing.isDestroyed();
|
||||
if (reopened) {
|
||||
createMainWindow('/dashboard');
|
||||
} else {
|
||||
win.show();
|
||||
win.focus();
|
||||
existing.show();
|
||||
existing.focus();
|
||||
}
|
||||
trackEvent('desktop_main_window_shown', { source, reopened });
|
||||
}
|
||||
|
||||
function registerShortcut(accelerator: string): void {
|
||||
|
|
@ -32,7 +35,7 @@ function registerShortcut(accelerator: string): void {
|
|||
}
|
||||
if (!accelerator) return;
|
||||
try {
|
||||
const ok = globalShortcut.register(accelerator, showMainWindow);
|
||||
const ok = globalShortcut.register(accelerator, () => showMainWindow('shortcut'));
|
||||
if (ok) {
|
||||
currentShortcut = accelerator;
|
||||
} else {
|
||||
|
|
@ -50,13 +53,19 @@ export async function createTray(): Promise<void> {
|
|||
tray.setToolTip('SurfSense');
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Open SurfSense', click: showMainWindow },
|
||||
{ label: 'Open SurfSense', click: () => showMainWindow('tray_menu') },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', click: () => { app.exit(0); } },
|
||||
{
|
||||
label: 'Quit',
|
||||
click: () => {
|
||||
trackEvent('desktop_tray_quit_clicked');
|
||||
app.exit(0);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
tray.setContextMenu(contextMenu);
|
||||
tray.on('double-click', showMainWindow);
|
||||
tray.on('double-click', () => showMainWindow('tray_click'));
|
||||
|
||||
const shortcuts = await getShortcuts();
|
||||
registerShortcut(shortcuts.generalAssist);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue