analytics/packages/analytics-client/src/device-collector.ts

133 lines
3.9 KiB
TypeScript

/**
* Device data collected from browser navigator APIs.
* This data is sent to the backend to enrich analytics.
*/
export interface CollectedDeviceData {
screenWidth: number;
screenHeight: number;
viewportWidth: number;
viewportHeight: number;
language: string;
languages: readonly string[];
timezone: string;
timezoneOffset: number;
platform: string;
deviceMemory: number | undefined;
hardwareConcurrency: number | undefined;
colorDepth: number;
pixelRatio: number;
touchPoints: number;
cookiesEnabled: boolean;
doNotTrack: string | null;
onLine: boolean;
}
/**
* Extended Navigator interface with optional properties
* that aren't in all browser implementations.
*/
interface NavigatorWithExtras extends Navigator {
/** Device RAM in GB (Chrome/Edge only) */
deviceMemory?: number;
/** Network connection info */
connection?: {
saveData: boolean;
effectiveType?: string;
};
}
/**
* Collect device data from browser navigator APIs.
* Returns null if running in a non-browser environment (SSR).
*
* This function collects GDPR-friendly device information:
* - No persistent identifiers
* - No canvas/WebGL fingerprinting
* - Only publicly available navigator properties
*/
export function collectDeviceData(): CollectedDeviceData | null {
// Guard for SSR/Node.js environments
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
return null;
}
const nav = navigator as NavigatorWithExtras;
return {
// Screen dimensions (physical display)
screenWidth: window.screen.width,
screenHeight: window.screen.height,
// Viewport dimensions (browser window content area)
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
// Locale information
language: navigator.language,
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timezoneOffset: new Date().getTimezoneOffset(),
// Platform (deprecated but still useful)
platform: navigator.platform,
// Device capabilities
deviceMemory: nav.deviceMemory,
hardwareConcurrency: navigator.hardwareConcurrency,
colorDepth: window.screen.colorDepth,
pixelRatio: window.devicePixelRatio,
touchPoints: navigator.maxTouchPoints,
// Privacy/capability flags
cookiesEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack,
onLine: navigator.onLine,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Memoized Version
// ─────────────────────────────────────────────────────────────────────────────
let cachedData: CollectedDeviceData | null = null;
let cacheInitialized = false;
/**
* Get device data with memoization.
* The data is collected once per session and cached.
*
* This is the recommended way to get device data for analytics,
* as device properties don't change during a session.
*/
export function getDeviceData(): CollectedDeviceData | null {
if (!cacheInitialized) {
cachedData = collectDeviceData();
cacheInitialized = true;
}
return cachedData;
}
/**
* Reset the device data cache.
* Useful for testing or when session changes.
*/
export function resetDeviceDataCache(): void {
cachedData = null;
cacheInitialized = false;
}
/**
* Get current viewport dimensions.
* Unlike getDeviceData(), this always returns fresh values.
* Use this for resize tracking.
*/
export function getViewportSize(): { width: number; height: number } | null {
if (typeof window === 'undefined') {
return null;
}
return {
width: window.innerWidth,
height: window.innerHeight,
};
}