Skip to content

Commit 879f67f

Browse files
authored
analytics: add IP headers collection for geo debugging and session tracking (#9648)
* analytics: add IP headers collection for geo debugging and session tracking Signed-off-by: Alexander Platov <[email protected]> * Fix Signed-off-by: Alexander Platov <[email protected]> * Format Signed-off-by: Alexander Platov <[email protected]> --------- Signed-off-by: Alexander Platov <[email protected]>
1 parent d1fd7d9 commit 879f67f

File tree

5 files changed

+248
-72
lines changed

5 files changed

+248
-72
lines changed

packages/analytics-providers/src/analyticsCollector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { type AnalyticProvider } from '@hcengineering/analytics'
1717
import presentation from '@hcengineering/presentation'
1818
import { getMetadata } from '@hcengineering/platform'
1919
import { AnalyticEventType } from '@hcengineering/analytics-collector'
20-
import { collectEventMetadata } from './utils'
20+
import { collectEventMetadata, triggerUrlChange } from './utils'
2121
import { type QueuedEvent } from './types'
2222

2323
export class AnalyticsCollectorProvider implements AnalyticProvider {
@@ -284,6 +284,7 @@ export class AnalyticsCollectorProvider implements AnalyticProvider {
284284
}
285285

286286
navigate (path: string): void {
287+
triggerUrlChange()
287288
this.addEvent(AnalyticEventType.Navigation, { path }, '$pageview')
288289
}
289290

packages/analytics-providers/src/utils.ts

Lines changed: 149 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ import { UAParser } from 'ua-parser-js'
1717
import { getMetadata } from '@hcengineering/platform'
1818
import presentation from '@hcengineering/presentation'
1919
import { desktopPlatform, getCurrentLocation } from '@hcengineering/ui'
20+
import { generateUuid } from '@hcengineering/core'
2021
import { Analytics } from '@hcengineering/analytics'
2122

22-
const parser = UAParser()
23-
2423
let _isSignUp: boolean = false
2524
export const signupStore = {
2625
setSignUpFlow: (isSignUp: boolean) => {
@@ -31,6 +30,115 @@ export const signupStore = {
3130
}
3231
}
3332

33+
// Session and activity tracking
34+
class SessionManager {
35+
private static instance: SessionManager
36+
private readonly sessionId: string
37+
private readonly windowId: string
38+
private readonly sessionStartTime: number
39+
private pageviewId: string
40+
private pageStartTime: number
41+
private eventCount: number = 0
42+
private pageViewCount: number = 0
43+
private currentUrl: string = ''
44+
45+
private constructor () {
46+
this.sessionId = generateUuid()
47+
this.windowId = generateUuid()
48+
this.sessionStartTime = Date.now()
49+
this.pageviewId = generateUuid()
50+
this.pageStartTime = Date.now()
51+
this.currentUrl = window.location.href
52+
this.pageViewCount = 1 // First page view
53+
54+
this.setupNavigationTracking()
55+
}
56+
57+
static getInstance (): SessionManager {
58+
if (SessionManager.instance === undefined) {
59+
SessionManager.instance = new SessionManager()
60+
}
61+
return SessionManager.instance
62+
}
63+
64+
private setupNavigationTracking (): void {
65+
window.addEventListener('popstate', () => {
66+
setTimeout(() => {
67+
this.onUrlChange()
68+
}, 0)
69+
})
70+
setInterval(() => {
71+
if (window.location.href !== this.currentUrl) {
72+
this.onUrlChange()
73+
}
74+
}, 5000)
75+
}
76+
77+
onUrlChange (): void {
78+
if (window.location.href !== this.currentUrl) {
79+
this.currentUrl = window.location.href
80+
this.pageviewId = generateUuid()
81+
this.pageStartTime = Date.now()
82+
this.pageViewCount++
83+
}
84+
}
85+
86+
getSessionData (): Record<string, any> {
87+
const now = Date.now()
88+
const timeOnPage = Math.floor((now - this.pageStartTime) / 1000)
89+
const sessionDuration = Math.floor((now - this.sessionStartTime) / 1000)
90+
91+
return {
92+
$session_id: this.sessionId,
93+
$window_id: this.windowId,
94+
$pageview_id: this.pageviewId,
95+
$session_duration: sessionDuration,
96+
$time_on_page: timeOnPage,
97+
$page_view_count: this.pageViewCount,
98+
$event_count: this.eventCount,
99+
$is_first_session: this.isFirstSession(),
100+
$is_returning_user: this.isReturningUser(),
101+
$insert_id: this.generateInsertId()
102+
}
103+
}
104+
105+
incrementEventCount (): void {
106+
this.eventCount++
107+
}
108+
109+
private generateInsertId (): string {
110+
const timestamp = Date.now()
111+
const random = Math.random().toString(36).substr(2, 9)
112+
return `${timestamp}-${random}`
113+
}
114+
115+
private isFirstSession (): boolean {
116+
const hasVisited = localStorage.getItem('analytics_has_visited')
117+
if (hasVisited === null || hasVisited === '') {
118+
localStorage.setItem('analytics_has_visited', 'true')
119+
return true
120+
}
121+
return false
122+
}
123+
124+
private isReturningUser (): boolean {
125+
const firstVisit = localStorage.getItem('analytics_first_visit')
126+
const now = Date.now()
127+
128+
if (firstVisit === null || firstVisit === '') {
129+
localStorage.setItem('analytics_first_visit', now.toString())
130+
return false
131+
}
132+
133+
const firstVisitTime = parseInt(firstVisit, 10)
134+
return now - firstVisitTime > 24 * 60 * 60 * 1000
135+
}
136+
}
137+
138+
export function triggerUrlChange (): void {
139+
SessionManager.getInstance().onUrlChange()
140+
}
141+
34142
function getUrlTrackingParams (): Record<string, string | null> {
35143
const params = new URLSearchParams(window.location.search)
36144
return {
@@ -78,7 +186,16 @@ function getDeviceType (type: string | undefined | null): string {
78186
}
79187

80188
export function collectEventMetadata (properties: Record<string, any> = {}): Record<string, any> {
81-
const trackingParams = getUrlTrackingParams()
189+
const parser = new UAParser()
190+
const browser = parser.getBrowser()
191+
const os = parser.getOS()
192+
const device = parser.getDevice()
193+
194+
const sessionManager = SessionManager.getInstance()
195+
sessionManager.incrementEventCount()
196+
const sessionData = sessionManager.getSessionData()
197+
const urlTracking = getUrlTrackingParams()
198+
const searchEngine = getSearchEngine(document.referrer)
82199

83200
const referrer = (() => {
84201
if (document.referrer === '') return '$direct'
@@ -104,32 +221,42 @@ export function collectEventMetadata (properties: Record<string, any> = {}): Rec
104221
const browserLanguagePrefix = browserLanguage.split('-')[0] !== '' ? browserLanguage.split('-')[0] : 'en'
105222

106223
return {
107-
...properties,
108-
$timestamp: new Date().toISOString(),
109-
$os: parser.os.name ?? 'Unknown OS',
110-
$os_version: parser.os.version ?? '',
111-
$browser: parser.browser.name ?? 'Unknown Browser',
112-
$browser_version: parseInt(parser.browser.major ?? '0', 10),
113-
$device_type: getDeviceType(parser.device.type),
114-
$current_url: window.location.href,
115-
$host: window.location.hostname,
116-
$pathname: window.location.pathname,
224+
title: document.title,
225+
// User agent and platform info
226+
$lib: desktopPlatform ? 'app' : 'web',
227+
$lib_version: getMetadata(presentation.metadata.FrontVersion) ?? '0.0.0',
228+
$browser: browser.name ?? 'Unknown Browser',
229+
$browser_version: parseInt(browser.major ?? '0', 10),
230+
$browser_language: browserLanguage,
231+
$browser_language_prefix: browserLanguagePrefix,
232+
$os: os.name ?? 'Unknown OS',
233+
$os_version: os.version,
234+
$device_type: getDeviceType(device.type),
235+
$device_model: device.model,
236+
$device_vendor: device.vendor,
117237
$screen_height: window.screen.height,
118238
$screen_width: window.screen.width,
119239
$viewport_height: window.innerHeight,
120240
$viewport_width: window.innerWidth,
121-
$lib: desktopPlatform ? 'app' : 'web',
122-
$lib_version: getMetadata(presentation.metadata.FrontVersion) ?? '0.0.0',
123-
$search_engine: getSearchEngine(referrer),
124-
$referrer: referrer,
125-
$referring_domain: referringDomain,
241+
$language: navigator.language,
126242
$raw_user_agent: navigator.userAgent,
127-
title: document.title,
128243
$timezone: timezone,
129244
$timezone_offset: timezoneOffset,
130-
$browser_language: browserLanguage,
131-
$browser_language_prefix: browserLanguagePrefix,
132-
...trackingParams
245+
$timestamp: new Date().toISOString(),
246+
// URL and referrer info
247+
$current_url: window.location.href,
248+
$host: window.location.host,
249+
$pathname: window.location.pathname,
250+
$search: window.location.search,
251+
referrer,
252+
$referring_domain: referringDomain,
253+
$search_engine: searchEngine,
254+
// Session and activity fields (PostHog-compatible)
255+
...sessionData,
256+
// URL tracking parameters (UTM, etc.)
257+
...urlTracking,
258+
// Custom event parameters
259+
...properties
133260
}
134261
}
135262

packages/ui/src/location.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ export function navigate (location: PlatformLocation, replace = false): boolean
203203
if (cur !== url) {
204204
const data = !desktopPlatform ? null : { location }
205205
const _url = !desktopPlatform ? url : undefined
206-
Analytics.navigate(url)
207206
if (replace) {
208207
history.replaceState(data, '', _url)
209208
} else {
@@ -214,6 +213,7 @@ export function navigate (location: PlatformLocation, replace = false): boolean
214213
localStorage.setItem(`${locationStorageKeyId}_${location.path[1]}`, JSON.stringify(location))
215214
}
216215
locationWritable.set(location)
216+
Analytics.navigate(url)
217217
return true
218218
}
219219
return false

0 commit comments

Comments
 (0)