From 2a1c28cc494680c531efd1b18cc2f818ac3a7b0c Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Mon, 17 Mar 2025 16:43:10 +0100 Subject: [PATCH] tracker: start tracker sdk --- .../tracker/src/main/app/analytics/events.ts | 7 + .../main/app/analytics/sharedProperties.ts | 71 ++++++ .../tracker/src/main/app/analytics/utils.ts | 230 ++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 tracker/tracker/src/main/app/analytics/events.ts create mode 100644 tracker/tracker/src/main/app/analytics/sharedProperties.ts create mode 100644 tracker/tracker/src/main/app/analytics/utils.ts diff --git a/tracker/tracker/src/main/app/analytics/events.ts b/tracker/tracker/src/main/app/analytics/events.ts new file mode 100644 index 000000000..7a7177ec0 --- /dev/null +++ b/tracker/tracker/src/main/app/analytics/events.ts @@ -0,0 +1,7 @@ +import SharedProperties from './sharedProperties.js' + +export default class Events { + constructor(private readonly sharedProperties: SharedProperties, private readonly token: string) {} + + sendEvent(eventName: string) {} +} \ No newline at end of file diff --git a/tracker/tracker/src/main/app/analytics/sharedProperties.ts b/tracker/tracker/src/main/app/analytics/sharedProperties.ts new file mode 100644 index 000000000..e19723030 --- /dev/null +++ b/tracker/tracker/src/main/app/analytics/sharedProperties.ts @@ -0,0 +1,71 @@ +import App from '../index.js' +import { uaParse } from './utils.js' + +const refKey = '$__initial_ref__$' +const distinctIdKey = '$__distinct_device_id__$' + +export default class SharedProperties { + os: string + browser: string + device: string + screenHeight: number + screenWidth: number + initialReferrer: string + utmSource: string | null + utmMedium: string | null + utmCampaign: string | null + deviceId: string + + constructor(private readonly app: App) { + const { width, height, browser, browserVersion, browserMajorVersion, os, osVersion, mobile } = + uaParse(window) + this.os = `${os} ${osVersion}` + this.browser = `${browser} ${browserVersion} (${browserMajorVersion})` + this.device = mobile ? 'Mobile' : 'Desktop' + this.screenHeight = height + this.screenWidth = width + this.initialReferrer = this.getReferrer() + const searchParams = new URLSearchParams(window.location.search) + this.utmSource = searchParams.get('utm_source') || null + this.utmMedium = searchParams.get('utm_medium') || null + this.utmCampaign = searchParams.get('utm_campaign') || null + this.deviceId = this.getDistinctDeviceId() + } + + get all() { + return { + os: this.os, + browser: this.browser, + device: this.device, + screenHeight: this.screenHeight, + screenWidth: this.screenWidth, + initialReferrer: this.initialReferrer, + utmSource: this.utmSource, + utmMedium: this.utmMedium, + utmCampaign: this.utmCampaign, + deviceId: this.deviceId, + } + } + + private getDistinctDeviceId() { + const potentialStored = this.app.localStorage.getItem(distinctIdKey) + if (potentialStored) { + return potentialStored + } else { + const distinctId = `${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}` + this.app.localStorage.setItem(distinctIdKey, distinctId) + return distinctId + } + } + + private getReferrer() { + const potentialStored = this.app.sessionStorage.getItem(refKey) + if (potentialStored) { + return potentialStored + } else { + const ref = document.referrer + this.app.sessionStorage.setItem(refKey, ref) + return ref + } + } +} diff --git a/tracker/tracker/src/main/app/analytics/utils.ts b/tracker/tracker/src/main/app/analytics/utils.ts new file mode 100644 index 000000000..74269eea5 --- /dev/null +++ b/tracker/tracker/src/main/app/analytics/utils.ts @@ -0,0 +1,230 @@ +interface ClientData { + screen: string + width: number + height: number + browser: string + browserVersion: string + browserMajorVersion: number + mobile: boolean + os: string + osVersion: string + cookies: boolean +} + +interface ClientOS { + s: string + r: RegExp +} + +/** + * Detects client browser, OS, and device information + */ +export function uaParse(window: Window & typeof globalThis): ClientData { + const unknown = '-' + + // Screen detection + let width: number = 0 + let height: number = 0 + let screenSize = '' + + if (screen.width) { + width = screen.width + height = screen.height + screenSize = `${width} x ${height}` + } + + // Browser detection + const nVer: string = navigator.appVersion + const nAgt: string = navigator.userAgent + let browser: string = navigator.appName + let version: string = String(parseFloat(nVer)) + let nameOffset: number + let verOffset: number + let ix: number + + // Browser detection logic + if ((verOffset = nAgt.indexOf('YaBrowser')) !== -1) { + browser = 'Yandex' + version = nAgt.substring(verOffset + 10) + } else if ((verOffset = nAgt.indexOf('SamsungBrowser')) !== -1) { + browser = 'Samsung' + version = nAgt.substring(verOffset + 15) + } else if ((verOffset = nAgt.indexOf('UCBrowser')) !== -1) { + browser = 'UC Browser' + version = nAgt.substring(verOffset + 10) + } else if ((verOffset = nAgt.indexOf('OPR')) !== -1) { + browser = 'Opera' + version = nAgt.substring(verOffset + 4) + } else if ((verOffset = nAgt.indexOf('Opera')) !== -1) { + browser = 'Opera' + version = nAgt.substring(verOffset + 6) + if ((verOffset = nAgt.indexOf('Version')) !== -1) { + version = nAgt.substring(verOffset + 8) + } + } else if ((verOffset = nAgt.indexOf('Edge')) !== -1) { + browser = 'Microsoft Legacy Edge' + version = nAgt.substring(verOffset + 5) + } else if ((verOffset = nAgt.indexOf('Edg')) !== -1) { + browser = 'Microsoft Edge' + version = nAgt.substring(verOffset + 4) + } else if ((verOffset = nAgt.indexOf('MSIE')) !== -1) { + browser = 'Microsoft Internet Explorer' + version = nAgt.substring(verOffset + 5) + } else if ((verOffset = nAgt.indexOf('Chrome')) !== -1) { + browser = 'Chrome' + version = nAgt.substring(verOffset + 7) + } else if ((verOffset = nAgt.indexOf('Safari')) !== -1) { + browser = 'Safari' + version = nAgt.substring(verOffset + 7) + if ((verOffset = nAgt.indexOf('Version')) !== -1) { + version = nAgt.substring(verOffset + 8) + } + } else if ((verOffset = nAgt.indexOf('Firefox')) !== -1) { + browser = 'Firefox' + version = nAgt.substring(verOffset + 8) + } else if (nAgt.indexOf('Trident/') !== -1) { + browser = 'Microsoft Internet Explorer' + version = nAgt.substring(nAgt.indexOf('rv:') + 3) + } else if ((nameOffset = nAgt.lastIndexOf(' ') + 1) < (verOffset = nAgt.lastIndexOf('/'))) { + browser = nAgt.substring(nameOffset, verOffset) + version = nAgt.substring(verOffset + 1) + if (browser.toLowerCase() === browser.toUpperCase()) { + browser = navigator.appName + } + } + + // Trim the version string + if ((ix = version.indexOf(';')) !== -1) { + version = version.substring(0, ix) + } + if ((ix = version.indexOf(' ')) !== -1) { + version = version.substring(0, ix) + } + if ((ix = version.indexOf(')')) !== -1) { + version = version.substring(0, ix) + } + + let majorVersion: number = parseInt(version, 10) + if (isNaN(majorVersion)) { + version = String(parseFloat(nVer)) + majorVersion = parseInt(nVer, 10) + } + + // Mobile detection + const mobile: boolean = /Mobile|mini|Fennec|Android|iP(ad|od|hone)/.test(nVer) + + // Cookie detection + let cookieEnabled: boolean = navigator.cookieEnabled || false + + if (typeof navigator.cookieEnabled === 'undefined' && !cookieEnabled) { + document.cookie = 'testcookie' + cookieEnabled = document.cookie.indexOf('testcookie') !== -1 + } + + // OS detection + let os: string = unknown + const clientStrings: ClientOS[] = [ + { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, + { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, + { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, + { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, + { s: 'Windows Vista', r: /Windows NT 6.0/ }, + { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, + { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, + { s: 'Windows 2000', r: /(Windows NT 5.0|Windows 2000)/ }, + { s: 'Windows ME', r: /(Win 9x 4.90|Windows ME)/ }, + { s: 'Windows 98', r: /(Windows 98|Win98)/ }, + { s: 'Windows 95', r: /(Windows 95|Win95|Windows_95)/ }, + { s: 'Windows NT 4.0', r: /(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/ }, + { s: 'Windows CE', r: /Windows CE/ }, + { s: 'Windows 3.11', r: /Win16/ }, + { s: 'Android', r: /Android/ }, + { s: 'Open BSD', r: /OpenBSD/ }, + { s: 'Sun OS', r: /SunOS/ }, + { s: 'Chrome OS', r: /CrOS/ }, + { s: 'Linux', r: /(Linux|X11(?!.*CrOS))/ }, + { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, + { s: 'Mac OS X', r: /Mac OS X/ }, + { s: 'Mac OS', r: /(Mac OS|MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, + { s: 'QNX', r: /QNX/ }, + { s: 'UNIX', r: /UNIX/ }, + { s: 'BeOS', r: /BeOS/ }, + { s: 'OS/2', r: /OS\/2/ }, + { + s: 'Search Bot', + r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/, + }, + ] + + // Find matching OS + for (const client of clientStrings) { + if (client.r.test(nAgt)) { + os = client.s + break + } + } + + // OS Version detection + let osVersion: string = unknown + + if (/Windows/.test(os)) { + const matches = /Windows (.*)/.exec(os) + if (matches && matches[1]) { + osVersion = matches[1] + // Handle Windows 10/11 detection with newer API if available + if (osVersion === '10' && 'userAgentData' in navigator) { + const nav = navigator as Navigator & { + userAgentData?: { + getHighEntropyValues(values: string[]): Promise<{ platformVersion: string }> + } + } + + if (nav.userAgentData) { + nav.userAgentData + .getHighEntropyValues(['platformVersion']) + .then((ua) => { + const version = parseInt(ua.platformVersion.split('.')[0], 10) + ;(window as any).jscd.osVersion = version < 13 ? '10' : '11' + }) + .catch(() => { + // Fallback if high entropy values not available + }) + } + } + } + os = 'Windows' + } + + // OS version detection for Mac/Android/iOS + switch (os) { + case 'Mac OS': + case 'Mac OS X': + case 'Android': { + const matches = + /(?:Android|Mac OS|Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh) ([\.\_\d]+)/.exec(nAgt) + osVersion = matches && matches[1] ? matches[1] : unknown + break + } + case 'iOS': { + const matches = /OS (\d+)_(\d+)_?(\d+)?/.exec(nVer) + if (matches && matches[1]) { + osVersion = `${matches[1]}.${matches[2]}.${parseInt(matches[3] || '0', 10)}` + } + break + } + } + + // Return client data + return { + screen: screenSize, + width, + height, + browser, + browserVersion: version, + browserMajorVersion: majorVersion, + mobile, + os, + osVersion, + cookies: cookieEnabled, + } +}