Skip to content

Commit baa1c53

Browse files
committed
feat(ga): Trace client events with ga.
1 parent 4a451ec commit baa1c53

File tree

5 files changed

+252
-1
lines changed

5 files changed

+252
-1
lines changed

src/ga.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {v4 as uuidv4} from 'uuid';
2+
import { machineIdSync } from './utils/machine-id';
3+
let cID = machineIdSync(false);
4+
5+
export class Analytics4 {
6+
private trackingID: string;
7+
private secretKey: string;
8+
private clientID: string;
9+
private sessionID: string;
10+
private customParams: Record<string, unknown> = {};
11+
private userProperties: Record<string, unknown> | null = null;
12+
13+
private baseURL = 'https://google-analytics.com/mp';
14+
private collectURL = '/collect';
15+
16+
constructor(trackingID: string, secretKey: string, clientID: string = cID, sessionID = uuidv4()) {
17+
this.trackingID = trackingID;
18+
this.secretKey = secretKey;
19+
this.clientID = clientID;
20+
this.sessionID = sessionID;
21+
}
22+
23+
set(key: string, value: any) {
24+
if (value !== null) {
25+
this.customParams[key] = value;
26+
} else {
27+
delete this.customParams[key];
28+
}
29+
30+
return this;
31+
}
32+
33+
setParams(params?: Record<string, unknown>) {
34+
if (typeof params === 'object' && Object.keys(params).length > 0) {
35+
Object.assign(this.customParams, params)
36+
} else {
37+
this.customParams = {};
38+
}
39+
40+
return this;
41+
}
42+
43+
setUserProperties(upValue?: Record<string, unknown>) {
44+
if (typeof upValue === 'object' && Object.keys(upValue).length > 0) {
45+
this.userProperties = upValue;
46+
} else {
47+
this.userProperties = null;
48+
}
49+
50+
return this;
51+
}
52+
53+
event(eventName: string): Promise<any> {
54+
const payload = {
55+
client_id: this.clientID,
56+
events: [
57+
{
58+
name: eventName,
59+
params: {
60+
session_id: this.sessionID,
61+
...this.customParams,
62+
},
63+
},
64+
],
65+
};
66+
67+
if(this.userProperties) {
68+
Object.assign(payload, {user_properties: this.userProperties})
69+
}
70+
71+
return fetch(
72+
`${this.baseURL}${this.collectURL}?measurement_id=${this.trackingID}&api_secret=${this.secretKey}`,
73+
{
74+
method: 'POST',
75+
body: JSON.stringify(payload)
76+
}
77+
)
78+
};
79+
80+
trace(eventName: string, params?: Record<string, unknown>) {
81+
this.setParams();
82+
this.setParams(params);
83+
this.event(eventName);
84+
this.setParams();
85+
}
86+
}
87+
88+
let gaInstance: Analytics4 = null;
89+
export const loadGA = () => {
90+
if (gaInstance) {
91+
return gaInstance;
92+
}
93+
gaInstance = new Analytics4('G-L8EE6ZNNG6', 's_RUrczOQYa99d7O-o8D7w');
94+
95+
gaInstance.setParams();
96+
gaInstance.setUserProperties();
97+
gaInstance.event('trace_init');
98+
return gaInstance;
99+
}
100+

src/hosting.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Utils from './utils';
66
import { HostServerUrl } from './remote';
77
import { Notice } from "obsidian";
88
import type { TransItemType } from "./i18n";
9+
import { loadGA } from './ga';
910
// Check hosting service
1011
export const checkRemoteHosting = async (plugin: InvioPlugin, dirname?: string) => {
1112
log.info('checking remote host service info: ', dirname);
@@ -51,21 +52,34 @@ export const setupS3HostConfig = async (plugin: InvioPlugin, config: Partial<S3C
5152

5253
export const syncWithRemoteProject = async (dirname: string, plugin: InvioPlugin) => {
5354
const settings = plugin.settings;
55+
const ga = loadGA();
5456
let projectInfo = await checkRemoteHosting(plugin, dirname);
5557
if (!projectInfo) {
5658
projectInfo = await new Promise((resolve, reject) => {
5759
const cb = async (project: any, err?: any) => {
5860
log.info('project created: ', project, err);
5961
if (err) {
62+
ga.trace('use_host_create_fail', {
63+
dirname,
64+
raw: err?.message || err,
65+
})
6066
reject(err);
6167
return;
6268
}
6369
if (!project) {
6470
// Error
6571
log.error('create project failed: ', project);
72+
ga.trace('use_host_create_fail', {
73+
dirname,
74+
raw: 'no project response',
75+
})
6676
reject('Project create failed');
6777
return;
6878
}
79+
ga.trace('use_host_create_done', {
80+
dirname,
81+
slug: project.slug,
82+
})
6983
return resolve(project);
7084
};
7185
const modal = new CreateProjectModal(plugin.app, plugin, dirname, dirname.toLowerCase(), '', null, cb.bind(plugin));
@@ -78,6 +92,13 @@ export const syncWithRemoteProject = async (dirname: string, plugin: InvioPlugin
7892
}
7993

8094
const { name, slug, password, endpoint, region, bucket, useHost: baseDomain } = projectInfo;
95+
ga.trace('use_host_sync', {
96+
bucket,
97+
region,
98+
name,
99+
slug,
100+
domain: baseDomain,
101+
});
81102
settings.hostConfig.hostPair = {
82103
dir: name,
83104
password,

src/main.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import icon, { UsingIconNames, getIconSvg, addIconForconflictFile } from './util
6363
import { StatsView, VIEW_TYPE_STATS, LogType } from "./statsView";
6464
import { syncWithRemoteProject, switchProject } from './hosting';
6565
import Utils from './utils';
66+
import { Analytics4, loadGA } from './ga';
6667

6768
const { iconNameSyncWait, iconNameSyncPending, iconNameSyncRunning, iconNameLogs, iconNameSyncLogo } = UsingIconNames;
6869
const Menu_Tab = ` `;
@@ -90,6 +91,7 @@ export default class InvioPlugin extends Plugin {
9091
i18n: I18n;
9192
vaultRandomID: string;
9293
recentSyncedFiles: any;
94+
ga: Analytics4;
9395

9496
isUnderWatch(file: TAbstractFile) {
9597
const rootDir = this.settings.localWatchDir;
@@ -154,6 +156,13 @@ export default class InvioPlugin extends Plugin {
154156
return;
155157
}
156158

159+
this.ga.trace('sync_run', {
160+
trigger: triggerSource,
161+
dirname: this.settings.localWatchDir,
162+
useHost: this.settings.useHost,
163+
fileNum: fileList?.length
164+
});
165+
157166
await this.checkIfRemoteProjectSync();
158167

159168
let originLabel = `${this.manifest.name}`;
@@ -588,6 +597,12 @@ export default class InvioPlugin extends Plugin {
588597
}-${Date.now()}: finish sync, triggerSource=${triggerSource}`
589598
);
590599

600+
this.ga.trace('sync_run_done', {
601+
trigger: triggerSource,
602+
dirname: this.settings.localWatchDir,
603+
useHost: this.settings.useHost,
604+
fileNum: fileList?.length
605+
})
591606
// TODO: Show stats model
592607
return toRemoteFiles;
593608
} catch (error) {
@@ -601,6 +616,14 @@ export default class InvioPlugin extends Plugin {
601616
loadingModal?.close();
602617
log.error(msg);
603618
log.error(error);
619+
this.ga.trace('sync_run_err', {
620+
trigger: triggerSource,
621+
msg,
622+
raw: error?.message,
623+
dirname: this.settings.localWatchDir,
624+
useHost: this.settings.useHost,
625+
fileNum: fileList?.length
626+
})
604627
getNotice(null, msg, 10 * 1000);
605628
if (error instanceof AggregateError) {
606629
for (const e of error.errors) {
@@ -626,7 +649,7 @@ export default class InvioPlugin extends Plugin {
626649

627650
async onload() {
628651
log.info(`loading plugin ${this.manifest.id}`);
629-
652+
this.ga = loadGA();
630653
// init html generator
631654
AssetHandler.initialize(this.manifest.id);
632655

@@ -698,6 +721,9 @@ export default class InvioPlugin extends Plugin {
698721
// Add custom icon for root dir
699722
setTimeout(() => {
700723
if (this.settings.localWatchDir) {
724+
this.ga.trace('boot_project', {
725+
dirname: this.settings.localWatchDir
726+
});
701727
this.switchWorkingDir(this.settings.localWatchDir);
702728
} else {
703729
new Notice(
@@ -730,6 +756,7 @@ export default class InvioPlugin extends Plugin {
730756
.setTitle(`${Menu_Tab}${t('menu_set_folder')}`)
731757
.setIcon("document")
732758
.onClick(async () => {
759+
this.ga.trace('switch_project', { dirname: file.path });
733760
await this.switchWorkingDir(file.path);
734761
});
735762
})
@@ -748,6 +775,7 @@ export default class InvioPlugin extends Plugin {
748775
.setIcon("document")
749776
.onClick(async () => {
750777
await InvioSettingTab.exportSettings(this)
778+
this.ga.trace('share_settings')
751779
});
752780
})
753781
}
@@ -858,6 +886,10 @@ export default class InvioPlugin extends Plugin {
858886
if (!this.settings.useHost) {
859887
return;
860888
}
889+
this.ga.trace('use_host_auth', {
890+
action,
891+
user
892+
});
861893
if (!this.settings.hostConfig) {
862894
this.settings.hostConfig = {} as THostConfig;
863895
}

src/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import os from 'os';
66
import { createHash } from 'crypto';
77
import { v4 } from 'uuid';
88
import { AppHostServerUrl } from './remoteForS3';
9+
import { loadGA } from './ga';
910

1011
const logger = console;
1112
logger.info = console.log;
@@ -70,6 +71,8 @@ const showNotification = async (opt: MessageBoxOptions) => {
7071

7172
const gotoAuth = (url?: string) => {
7273
(window as any).electron.remote.shell.openExternal(url || `${AppHostServerUrl}/exporter`);
74+
const ga = loadGA();
75+
ga?.trace('use_host_login');
7376
}
7477

7578
const gotoMainSite = () => {

src/utils/machine-id.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/* @flow */
2+
import { exec, execSync } from 'child_process';
3+
import { createHash } from 'crypto';
4+
import { nanoid } from 'nanoid'
5+
import { existsSync } from 'fs';
6+
7+
type NodeJSPlatform = 'aix' | 'darwin' | 'freebsd' | 'linux' | 'openbsd' | 'sunos' | 'win32';
8+
9+
function parseOSFromUA(userAgent: string): NodeJSPlatform | undefined {
10+
const osRegex = /(Windows NT|Mac OS X|Linux|Android|iOS|CrOS)[/ ]([\d._]+)/;
11+
const match = userAgent.match(osRegex);
12+
13+
if (match && match.length >= 3) {
14+
const osName = match[1].toLowerCase();
15+
switch (osName) {
16+
case 'windows nt':
17+
return 'win32';
18+
case 'mac os x':
19+
return 'darwin';
20+
case 'linux':
21+
return 'linux';
22+
case 'android':
23+
return 'linux';
24+
case 'ios':
25+
return 'darwin';
26+
case 'cros':
27+
return 'linux';
28+
default:
29+
return undefined;
30+
}
31+
}
32+
33+
return undefined;
34+
}
35+
36+
const platform = process?.platform || parseOSFromUA(navigator.userAgent);
37+
console.log('get platform: ', platform);
38+
39+
const getWin32BinPath = () => {
40+
let binPath = '%windir%\\System32\\REG.exe';
41+
if (!existsSync(binPath)) {
42+
binPath = '%windir%\\sysnative\\cmd.exe /c %windir%\\System32\\REG.exe'
43+
}
44+
return binPath;
45+
}
46+
47+
const guid: Record<string, string> = {
48+
darwin: 'ioreg -rd1 -c IOPlatformExpertDevice',
49+
win32: getWin32BinPath() +
50+
' QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography ' +
51+
'/v MachineGuid',
52+
linux: '( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :',
53+
freebsd: 'kenv -q smbios.system.uuid || sysctl -n kern.hostuuid'
54+
};
55+
56+
57+
function hash(guid: string): string {
58+
return createHash('sha256').update(guid).digest('hex');
59+
}
60+
61+
function expose(result: string): string {
62+
switch (platform) {
63+
case 'darwin':
64+
return result
65+
.split('IOPlatformUUID')[1]
66+
.split('\n')[0].replace(/\=|\s+|\"/ig, '')
67+
.toLowerCase();
68+
case 'win32':
69+
return result
70+
.toString()
71+
.split('REG_SZ')[1]
72+
.replace(/\r+|\n+|\s+/ig, '')
73+
.toLowerCase();
74+
case 'linux':
75+
return result
76+
.toString()
77+
.replace(/\r+|\n+|\s+/ig, '')
78+
.toLowerCase();
79+
case 'freebsd':
80+
return result
81+
.toString()
82+
.replace(/\r+|\n+|\s+/ig, '')
83+
.toLowerCase();
84+
default:
85+
throw new Error(`Unsupported platform: ${platform}`);
86+
}
87+
}
88+
89+
export function machineIdSync(original: boolean): string {
90+
if (!platform) {
91+
return 'fakeid' + nanoid();
92+
}
93+
let id: string = expose(execSync(guid[platform]).toString());
94+
return original ? id : hash(id);
95+
}

0 commit comments

Comments
 (0)