solo-analytics 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,391 +1,120 @@
1
- # 📊 Solo Analytics
1
+ # Solo Analytics
2
2
 
3
- A lightweight TypeScript library for comprehensive browser and device information collection with a clean API surface.
4
-
5
- [![npm version](https://img.shields.io/npm/v/solo-analytics.svg?style=flat-square)](https://www.npmjs.com/package/solo-analytics)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue?style=flat-square)](https://www.typescriptlang.org/)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://github.com/cesswhite/solo-analytics/blob/main/LICENSE)
8
- [![Bundle Size](https://img.shields.io/bundlephobia/minzip/solo-analytics?style=flat-square)](https://bundlephobia.com/package/solo-analytics)
9
-
10
- ## Features
11
-
12
- - 🔍 Extensive browser, OS, and device detection with full TypeScript support
13
- - 📱 Precise mobile, tablet, and desktop device classification
14
- - 🌐 Network connectivity and performance data
15
- - 📏 Screen and viewport dimensions with orientation detection
16
- - ⚡ Comprehensive performance metrics (navigation, timing, memory)
17
- - 🌍 Locale, timezone, and environment information
18
- - ⚙️ Advanced feature detection (camera, microphone, battery, etc.)
19
- - 🔒 Incognito/private mode detection
20
- - 🔄 Auto-refresh capability for dynamic metrics
21
-
22
- ## Installation
3
+ Client-side library that reads browser APIs and returns structured device, network, and environment data. One function call, typed output, no backend required.
23
4
 
24
5
  ```bash
25
6
  npm install solo-analytics
26
7
  ```
27
8
 
28
- ## Basic Usage
29
-
30
- ```typescript
31
- import { useSoloAnalytics } from "solo-analytics";
32
-
33
- // Initialize with default options
34
- const analytics = useSoloAnalytics();
35
-
36
- // Access common properties
37
- console.log("Mobile device:", analytics.isMobile);
38
- console.log("Browser:", analytics.browserName);
39
- console.log("OS:", analytics.osName);
40
-
41
- // Access the complete data object
42
- console.log("Full analytics data:", analytics.data);
43
- ```
44
-
45
- ## Configuration Options
46
-
47
- ```typescript
48
- import { useSoloAnalytics } from "solo-analytics";
49
-
50
- const analytics = useSoloAnalytics({
51
- // Automatically refresh dynamic data (network, performance)
52
- autoRefresh: true,
53
- // Refresh interval in milliseconds (default: 30000)
54
- refreshInterval: 10000,
55
- // Track page visibility changes
56
- trackVisibility: true,
57
- });
58
- ```
59
-
60
- ## API Reference
9
+ ## Why this exists
61
10
 
62
- The `useSoloAnalytics()` function returns an object with the following methods and properties:
11
+ Most analytics SDKs are built to send events to a vendor. Solo Analytics does something narrower: it collects **context** from the browser—user agent, screen size, connection quality, locale, performance timing—and gives it back to you as a plain object.
63
12
 
64
- ### Methods
13
+ Use it when you need to:
65
14
 
66
- - `refresh()`: Manually triggers a refresh of all analytics data
67
- - `destroy()`: Cleans up all event listeners and timers to prevent memory leaks
15
+ - Attach device context to your own analytics pipeline
16
+ - Adapt UI or features based on connection, screen, or device type
17
+ - Debug client environments without sprinkling `navigator` checks across your app
68
18
 
69
- ### Convenience Properties
19
+ It is not a tracking product. It does not phone home, store cookies, or manage sessions. You decide what to do with the data.
70
20
 
71
- - `isMobile`: Whether the device is a mobile phone
72
- - `isTablet`: Whether the device is a tablet
73
- - `isDesktop`: Whether the device is a desktop computer
74
- - `isOnline`: Current network connection state
75
- - `isVisible`: Whether the page is currently visible
76
- - `browserName`: The name of the browser
77
- - `osName`: The name of the operating system
21
+ ## What you get
78
22
 
79
- ### Complete Data
23
+ | Area | Data |
24
+ | --------------------- | ---------------------------------------------------------------- |
25
+ | Browser / OS / device | Parsed UA, engine, vendor, mobile/tablet/desktop classification |
26
+ | Screen | Dimensions, pixel ratio, orientation, touch points |
27
+ | Network | Online status, effective type, downlink, RTT, save-data flag |
28
+ | Performance | Navigation type, load timing, paint metrics, memory (Chrome) |
29
+ | Environment | Timezone, languages, storage availability, DNT, iframe detection |
30
+ | Optional probes | Incognito hint, camera/mic presence, battery, permission states |
80
31
 
81
- - `data`: An object containing all detailed analytics information
32
+ Sync data (UA, screen, locale) is collected immediately. Heavier async probes run on idle time by default so they stay off the critical path.
82
33
 
83
- ## Data Structure
84
-
85
- The `data` object contains the following structure:
34
+ ## Usage
86
35
 
87
36
  ```typescript
88
- interface SoloAnalyticsInfo {
89
- browser: {
90
- name: string;
91
- version: string;
92
- major: string;
93
- userAgent: string;
94
- vendor: string;
95
- engine: string;
96
- engineVersion: string;
97
- };
98
- os: {
99
- name: string;
100
- version: string;
101
- architecture: string;
102
- };
103
- device: {
104
- type: "mobile" | "tablet" | "desktop" | "unknown";
105
- vendor: string;
106
- model: string;
107
- orientation: "portrait" | "landscape";
108
- isMobile: boolean;
109
- isTablet: boolean;
110
- isDesktop: boolean;
111
- touch: boolean;
112
- };
113
- network: {
114
- online: boolean;
115
- effectiveType: string;
116
- downlink: number;
117
- rtt: number;
118
- saveData: boolean;
119
- };
120
- screen: {
121
- width: number;
122
- height: number;
123
- availWidth: number;
124
- availHeight: number;
125
- colorDepth: number;
126
- orientation: string;
127
- pixelRatio: number;
128
- touchPoints: number;
129
- };
130
- performance: {
131
- memory: {
132
- jsHeapSizeLimit: number;
133
- totalJSHeapSize: number;
134
- usedJSHeapSize: number;
135
- } | null;
136
- navigation: {
137
- type: string;
138
- redirectCount: number;
139
- };
140
- timing: {
141
- loadTime: number;
142
- domContentLoaded: number;
143
- firstPaint: number | null;
144
- firstContentfulPaint: number | null;
145
- };
146
- };
147
- location: {
148
- timeZone: string;
149
- language: string;
150
- languages: string[];
151
- isRestricted: boolean;
152
- doNotTrack: boolean | null;
153
- cookiesEnabled: boolean;
154
- localStorage: boolean;
155
- sessionStorage: boolean;
156
- };
157
- pageVisibility: "visible" | "hidden";
158
- referrer: string;
159
- isIncognito: boolean;
160
- hasCamera: boolean | null;
161
- hasMicrophone: boolean | null;
162
- hasBattery: boolean | null;
163
- batteryLevel: number | null;
164
- batteryCharging: boolean | null;
165
- permissions: Record<string, string>;
166
- }
167
- ```
168
-
169
- ## Framework Integration Examples
170
-
171
- ### React
172
-
173
- ```tsx
174
- import { useEffect, useState } from "react";
175
- import { useSoloAnalytics, SoloAnalyticsReturn } from "solo-analytics";
176
-
177
- function DeviceInfo() {
178
- const [analytics, setAnalytics] = useState<SoloAnalyticsReturn | null>(null);
179
-
180
- useEffect(() => {
181
- // Initialize on the client side
182
- const analyticsInstance = useSoloAnalytics();
183
- setAnalytics(analyticsInstance);
184
-
185
- // Cleanup on unmount
186
- return () => {
187
- analyticsInstance.destroy();
188
- };
189
- }, []);
190
-
191
- if (!analytics) return <div>Loading...</div>;
192
-
193
- return (
194
- <div>
195
- <h1>Device Information</h1>
196
- <p>
197
- Type:{" "}
198
- {analytics.isMobile
199
- ? "Mobile"
200
- : analytics.isTablet
201
- ? "Tablet"
202
- : "Desktop"}
203
- </p>
204
- <p>Browser: {analytics.browserName}</p>
205
- <p>Operating System: {analytics.osName}</p>
206
- <p>Network Status: {analytics.isOnline ? "Online" : "Offline"}</p>
207
- </div>
208
- );
209
- }
210
- ```
211
-
212
- ### Vue 3
213
-
214
- ```vue
215
- <template>
216
- <div>
217
- <h1>Device Information</h1>
218
- <p>Type: {{ deviceType }}</p>
219
- <p>Browser: {{ analytics.browserName }}</p>
220
- <p>Operating System: {{ analytics.osName }}</p>
221
- <p>Network Status: {{ analytics.isOnline ? "Online" : "Offline" }}</p>
222
- </div>
223
- </template>
224
-
225
- <script setup lang="ts">
226
- import { onMounted, onUnmounted, ref, computed } from "vue";
227
- import { useSoloAnalytics, SoloAnalyticsReturn } from "solo-analytics";
228
-
229
- const analytics = ref<SoloAnalyticsReturn | null>(null);
230
-
231
- onMounted(() => {
232
- analytics.value = useSoloAnalytics({
233
- trackVisibility: true,
234
- });
235
- });
37
+ import { useSoloAnalytics } from "solo-analytics";
236
38
 
237
- onUnmounted(() => {
238
- analytics.value?.destroy();
239
- });
39
+ const analytics = useSoloAnalytics();
240
40
 
241
- const deviceType = computed(() => {
242
- if (!analytics.value) return "Unknown";
41
+ console.log(analytics.isMobile);
42
+ console.log(analytics.browserName);
43
+ console.log(analytics.data);
243
44
 
244
- if (analytics.value.isMobile) return "Mobile";
245
- if (analytics.value.isTablet) return "Tablet";
246
- return "Desktop";
247
- });
248
- </script>
45
+ // When done (SPA unmount, etc.)
46
+ analytics.destroy();
249
47
  ```
250
48
 
251
- ### Nuxt 3
252
-
253
- ```vue
254
- <template>
255
- <div>
256
- <h1>Device Information</h1>
257
- <div v-if="analytics">
258
- <p>Type: {{ deviceType }}</p>
259
- <p>Browser: {{ analytics.browserName }}</p>
260
- <p>Operating System: {{ analytics.osName }}</p>
261
- <p>Network Status: {{ analytics.isOnline ? "Online" : "Offline" }}</p>
262
- </div>
263
- <div v-else>
264
- <p>Loading device information...</p>
265
- </div>
266
- </div>
267
- </template>
268
-
269
- <script setup lang="ts">
270
- import { ref, computed } from "vue";
271
- import { useSoloAnalytics, SoloAnalyticsReturn } from "solo-analytics";
272
-
273
- const analytics = ref<SoloAnalyticsReturn | null>(null);
274
-
275
- // Important: Only initialize on client-side to avoid SSR issues
276
- onMounted(() => {
277
- analytics.value = useSoloAnalytics({
278
- trackVisibility: true,
279
- autoRefresh: true,
280
- refreshInterval: 15000,
281
- });
282
- });
283
-
284
- onBeforeUnmount(() => {
285
- if (analytics.value) {
286
- analytics.value.destroy();
287
- }
288
- });
49
+ Despite the name, `useSoloAnalytics` is framework-agnostic. The `use` prefix matches Vue conventions, but it works the same in React, Nuxt, or plain JS—initialize on the client, call `destroy()` on teardown.
289
50
 
290
- const deviceType = computed(() => {
291
- if (!analytics.value) return "Unknown";
51
+ ### Options
292
52
 
293
- if (analytics.value.isMobile) return "Mobile";
294
- if (analytics.value.isTablet) return "Tablet";
295
- return "Desktop";
53
+ ```typescript
54
+ const analytics = useSoloAnalytics({
55
+ autoRefresh: false, // poll network + performance (default: false)
56
+ refreshInterval: 30000, // ms between polls when autoRefresh is true
57
+ trackVisibility: true, // listen for visibilitychange
58
+ detectFeatures: true, // run async probes (incognito, battery, media, permissions)
59
+ lazyFeatures: true, // defer async probes via requestIdleCallback (default: true)
296
60
  });
297
- </script>
298
61
  ```
299
62
 
300
- ### Vanilla JavaScript
63
+ Set `detectFeatures: false` if you only need UA, screen, and network data. That skips the async work entirely.
301
64
 
302
- ```html
303
- <script type="module">
304
- import { useSoloAnalytics } from "solo-analytics";
65
+ ### API
305
66
 
306
- document.addEventListener("DOMContentLoaded", () => {
307
- const analytics = useSoloAnalytics();
67
+ | Member | Description |
68
+ | ----------------------------------- | ------------------------------------------------------------------ |
69
+ | `data` | Full `SoloAnalyticsInfo` object (see `src/types/soloAnalytics.ts`) |
70
+ | `refresh()` | Re-collect sync, dynamic, and async data |
71
+ | `destroy()` | Clear timers and event listeners |
72
+ | `isMobile`, `isTablet`, `isDesktop` | Device class shortcuts |
73
+ | `isOnline` | `navigator.onLine` |
74
+ | `isVisible` | Page visibility state |
75
+ | `browserName`, `osName` | Parsed names |
308
76
 
309
- // Display information on the page
310
- document.getElementById("deviceType").textContent = analytics.isMobile
311
- ? "Mobile"
312
- : analytics.isTablet
313
- ? "Tablet"
314
- : "Desktop";
77
+ Types are exported: `SoloAnalyticsInfo`, `SoloAnalyticsOptions`, `SoloAnalyticsReturn`, and per-section interfaces.
315
78
 
316
- document.getElementById(
317
- "browserInfo"
318
- ).textContent = `${analytics.browserName} on ${analytics.osName}`;
79
+ ### SSR
319
80
 
320
- // Cleanup on window close
321
- window.addEventListener("beforeunload", () => {
322
- analytics.destroy();
323
- });
324
- });
325
- </script>
326
- ```
81
+ The library guards against missing `window` / `navigator`. On the server, `data` stays at its initial empty state. Initialize in `onMounted` (Vue/Nuxt) or `useEffect` (React), not during SSR.
82
+
83
+ ### Limitations
327
84
 
328
- ## Browser Compatibility
85
+ Be aware of what this library can and cannot guarantee:
329
86
 
330
- Solo Analytics is compatible with all modern browsers, including:
87
+ | Topic | Reality |
88
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
89
+ | UA parsing | Regex-based, not a full UA database. Edge cases exist (iPad desktop mode, new browsers, reduced UA strings). |
90
+ | Client Hints | `navigator.userAgentData` is not used yet. |
91
+ | Performance timing | Uses Navigation Timing where available; some fields are Chrome-only (`performance.memory`). |
92
+ | Incognito detection | Best-effort hint via IndexedDB — unreliable across browsers. |
93
+ | Camera / microphone | `enumerateDevices` may return empty until the user grants permission. `false` does not always mean absent. |
94
+ | Mutable `data` | The `data` object updates in place. There is no reactive subscription — call `refresh()` or read getters after you know data changed. |
331
95
 
332
- - Chrome 60+
333
- - Firefox 55+
334
- - Safari 11+
335
- - Edge 16+
336
- - Opera 47+
337
- - iOS Safari 11+
338
- - Android Browser 67+
96
+ ## Bundle size
339
97
 
340
- Some advanced features may not be available in older browsers but the library includes graceful fallbacks.
98
+ Roughly 3–4 KB gzipped. `sideEffects: false` in `package.json` so bundlers can tree-shake unused exports.
341
99
 
342
100
  ## Development
343
101
 
102
+ This project uses [Vite+](https://viteplus.dev/) (Vite, Vitest, Oxlint, Oxfmt, tsdown) via the `vp` CLI.
103
+
344
104
  ```bash
345
- # Install dependencies
346
105
  npm install
347
-
348
- # Run development server
349
- npm run dev
350
-
351
- # Build for production
352
- npm run build
106
+ vp dev # local demo playground
107
+ vp pack # build library (ESM + CJS + bundled types, publint + attw)
108
+ vp test # unit tests
109
+ vp test run --coverage
110
+ vp check # format, lint, and type-check
111
+ vp check --fix # auto-fix format/lint issues
353
112
  ```
354
113
 
355
114
  ## Contributing
356
115
 
357
- Contributions are welcome! Please feel free to submit a Pull Request.
358
-
359
- 1. Fork the repository
360
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
361
- 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
362
- 4. Push to the branch (`git push origin feature/amazing-feature`)
363
- 5. Open a Pull Request
364
-
365
- ## Repository
366
-
367
- GitHub: [https://github.com/cesswhite/solo-analytics](https://github.com/cesswhite/solo-analytics)
116
+ Issues and pull requests are welcome on [GitHub](https://github.com/cesswhite/solo-analytics).
368
117
 
369
118
  ## License
370
119
 
371
- MIT License
372
-
373
- Copyright (c) 2025 Céss White
374
-
375
- Permission is hereby granted, free of charge, to any person obtaining a copy
376
- of this software and associated documentation files (the "Software"), to deal
377
- in the Software without restriction, including without limitation the rights
378
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
379
- copies of the Software, and to permit persons to whom the Software is
380
- furnished to do so, subject to the following conditions:
381
-
382
- The above copyright notice and this permission notice shall be included in all
383
- copies or substantial portions of the Software.
384
-
385
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
386
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
387
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
388
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
389
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
390
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
391
- SOFTWARE.
120
+ [MIT](LICENSE) — Copyright (c) 2025 Céss White
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});async function e(){return new Promise(e=>{if(typeof window>`u`||typeof indexedDB>`u`){e(!1);return}let t=!1,n=n=>{t||(t=!0,clearTimeout(r),e(n))},r=setTimeout(()=>n(!1),1e3),i=indexedDB.open(`test`);i.onerror=()=>n(!0),i.onsuccess=()=>{i.result?.close(),n(!1)}})}async function t(){if(typeof navigator>`u`||!navigator.mediaDevices||!navigator.mediaDevices.enumerateDevices)return{hasCamera:null,hasMicrophone:null};try{let e=await navigator.mediaDevices.enumerateDevices();return{hasCamera:e.some(e=>e.kind===`videoinput`),hasMicrophone:e.some(e=>e.kind===`audioinput`)}}catch{return{hasCamera:null,hasMicrophone:null}}}async function n(){if(typeof navigator>`u`||!navigator.getBattery)return{hasBattery:null,level:null,charging:null};try{let e=await navigator.getBattery();return{hasBattery:!0,level:e.level,charging:e.charging}}catch{return{hasBattery:null,level:null,charging:null}}}async function r(){if(typeof navigator>`u`||!navigator.permissions||!navigator.permissions.query)return{};let e={},t=await Promise.all([`geolocation`,`notifications`,`push`,`midi`,`camera`,`microphone`,`background-sync`,`accelerometer`,`gyroscope`,`magnetometer`].map(async e=>{try{return[e,(await navigator.permissions.query({name:e})).state]}catch{return[e,`not-supported`]}}));for(let[n,r]of t)e[n]=r;return e}function i(){if(typeof window>`u`||typeof navigator>`u`)return{timeZone:``,language:``,languages:[],isRestricted:!1,doNotTrack:null,cookiesEnabled:!1,localStorage:!1,sessionStorage:!1};let e=Intl.DateTimeFormat().resolvedOptions().timeZone,t=navigator.language||``,n=navigator.languages?Array.from(navigator.languages):[t],r=!1;try{r=window.self!==window.top}catch{r=!0}let i=null;navigator.doNotTrack===`1`||navigator.doNotTrack===`yes`?i=!0:(navigator.doNotTrack===`0`||navigator.doNotTrack===`no`)&&(i=!1);let a=navigator.cookieEnabled,o=e=>{try{let t=window[e],n=`__test_${e}__`;return t.setItem(n,`test`),t.removeItem(n),!0}catch{return!1}};return{timeZone:e,language:t,languages:n,isRestricted:r,doNotTrack:i,cookiesEnabled:a,localStorage:o(`localStorage`),sessionStorage:o(`sessionStorage`)}}function a(){let e=typeof navigator<`u`?navigator.onLine:!1,t=`unknown`,n=0,r=0,i=!1;if(navigator?.connection){let e=navigator.connection;t=e.effectiveType||t,n=e.downlink||n,r=e.rtt||r,i=e.saveData||i}return{online:e,effectiveType:t,downlink:n,rtt:r,saveData:i}}function o(){return typeof window>`u`?{memory:null,navigation:{type:`Unknown`,redirectCount:0},timing:{loadTime:0,domContentLoaded:0,firstPaint:null,firstContentfulPaint:null}}:{memory:(()=>{if(window.performance?.memory){let e=window.performance.memory;return{jsHeapSizeLimit:e.jsHeapSizeLimit,totalJSHeapSize:e.totalJSHeapSize,usedJSHeapSize:e.usedJSHeapSize}}return null})(),navigation:window.performance?.navigation?{type:[`navigate`,`reload`,`back_forward`,`prerender`][window.performance.navigation.type]||`Unknown`,redirectCount:window.performance.navigation.redirectCount}:{type:`Unknown`,redirectCount:0},timing:(()=>{if(!window.performance?.timing)return{loadTime:0,domContentLoaded:0,firstPaint:null,firstContentfulPaint:null};let e=window.performance.timing,t=e.loadEventEnd-e.navigationStart,n=e.domContentLoadedEventEnd-e.navigationStart,r=null,i=null;if(window.performance&&typeof window.performance.getEntriesByType==`function`){let e=window.performance.getEntriesByType(`paint`),t=e.find(e=>e.name===`first-paint`),n=e.find(e=>e.name===`first-contentful-paint`);t&&(r=t.startTime),n&&(i=n.startTime)}return{loadTime:t,domContentLoaded:n,firstPaint:r,firstContentfulPaint:i}})()}}function s(e,t=2e3){if(typeof window>`u`){Promise.resolve().then(e);return}let n=()=>{Promise.resolve(e())};if(`requestIdleCallback`in window){window.requestIdleCallback(()=>n(),{timeout:t});return}setTimeout(n,0)}function c(){if(typeof window>`u`||typeof screen>`u`)return{width:0,height:0,availWidth:0,availHeight:0,colorDepth:0,orientation:`unknown`,pixelRatio:1,touchPoints:0};let e=`unknown`;e=window.innerHeight>window.innerWidth?`portrait`:`landscape`,screen.orientation?.type&&(e=screen.orientation.type);let t=window.devicePixelRatio||1,n=navigator.maxTouchPoints||0;return{width:screen.width,height:screen.height,availWidth:screen.availWidth,availHeight:screen.availHeight,colorDepth:screen.colorDepth,orientation:e,pixelRatio:t,touchPoints:n}}function l(e,t={}){let n=t.vendor??(typeof navigator<`u`?navigator.vendor:``),r=t.innerWidth??(typeof window<`u`?window.innerWidth:1024),i=t.innerHeight??(typeof window<`u`?window.innerHeight:768),a=t.maxTouchPoints??(typeof navigator<`u`&&`maxTouchPoints`in navigator?navigator.maxTouchPoints:0);return{browser:(()=>{for(let t of[{name:`Edge`,regex:/Edg(?:e|A|iOS)?\/([0-9.]+)/},{name:`Samsung Browser`,regex:/SamsungBrowser\/([0-9.]+)/},{name:`Opera`,regex:/(?:Opera|OPR)\/([0-9.]+)/},{name:`Firefox`,regex:/Firefox\/([0-9.]+)/},{name:`Chrome`,regex:/Chrome\/([0-9.]+)/},{name:`Safari`,regex:/Version\/([0-9.]+).*Safari/},{name:`IE`,regex:/MSIE|Trident/}]){let r=e.match(t.regex);if(r){let i=r[1]||``,a=i.split(`.`)[0]||``,o=`Unknown`,s=``;if(e.includes(`AppleWebKit`)){let t=e.match(/AppleWebKit\/([0-9.]+)/);o=`WebKit`,s=t?t[1]:``}else if(e.includes(`Gecko`)){o=`Gecko`;let t=e.match(/rv:([0-9.]+)/);s=t?t[1]:``}else if(e.includes(`Trident`)){o=`Trident`;let t=e.match(/Trident\/([0-9.]+)/);s=t?t[1]:``}return{name:t.name,version:i,major:a,userAgent:e,vendor:n,engine:o,engineVersion:s}}}return{name:`Unknown`,version:``,major:``,userAgent:e,vendor:n,engine:`Unknown`,engineVersion:``}})(),os:(()=>{for(let t of[{name:`iOS`,regex:/iPhone|iPad|iPod/},{name:`Android`,regex:/Android ([0-9.]+)/},{name:`Windows`,regex:/Windows NT ([0-9.]+)/},{name:`macOS`,regex:/Mac OS X ([0-9_.]+)/},{name:`Linux`,regex:/Linux/}]){let n=e.match(t.regex);if(n){let r=``;return n[1]&&(r=t.name===`macOS`?n[1].replace(/_/g,`.`):n[1]),{name:t.name,version:r,architecture:e.includes(`x64`)||e.includes(`x86_64`)?`64-bit`:`32-bit`}}}return{name:`Unknown`,version:``,architecture:e.includes(`x64`)||e.includes(`x86_64`)?`64-bit`:`32-bit`}})(),device:(()=>{let t=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e),n=/iPad|Android(?!.*Mobile)/i.test(e),o=`Unknown`,s=`Unknown`;if(e.includes(`iPhone`)||e.includes(`iPad`)||e.includes(`iPod`))o=`Apple`,e.includes(`iPhone`)&&(s=`iPhone`),e.includes(`iPad`)&&(s=`iPad`),e.includes(`iPod`)&&(s=`iPod`);else if(e.includes(`Samsung`))o=`Samsung`;else if(e.includes(`Pixel`)){o=`Google`;let t=e.match(/Pixel ([0-9XL]+)/);t&&(s=`Pixel ${t[1]}`)}let c=n?`tablet`:t?`mobile`:`desktop`;return{type:c,vendor:o,model:s,orientation:i>r?`portrait`:`landscape`,isMobile:c===`mobile`,isTablet:c===`tablet`,isDesktop:c===`desktop`,touch:a>0}})()}}const u=()=>({browser:{name:``,version:``,major:``,userAgent:``,vendor:``,engine:``,engineVersion:``},os:{name:``,version:``,architecture:``},device:{type:`unknown`,vendor:``,model:``,orientation:`portrait`,isMobile:!1,isTablet:!1,isDesktop:!1,touch:!1},network:{online:!1,effectiveType:``,downlink:0,rtt:0,saveData:!1},screen:{width:0,height:0,availWidth:0,availHeight:0,colorDepth:0,orientation:``,pixelRatio:1,touchPoints:0},performance:{memory:null,navigation:{type:``,redirectCount:0},timing:{loadTime:0,domContentLoaded:0,firstPaint:null,firstContentfulPaint:null}},location:{timeZone:``,language:``,languages:[],isRestricted:!1,doNotTrack:null,cookiesEnabled:!1,localStorage:!1,sessionStorage:!1},pageVisibility:`visible`,referrer:``,isIncognito:!1,hasCamera:null,hasMicrophone:null,hasBattery:null,batteryLevel:null,batteryCharging:null,permissions:{}});function d(d={}){let{autoRefresh:f=!1,refreshInterval:p=3e4,trackVisibility:m=!0,detectFeatures:h=!0,lazyFeatures:g=!0}=d,_=u(),v=null,y=null,b=!1,x=()=>{if(typeof navigator>`u`||typeof window>`u`)return;let e=navigator.userAgent,{browser:t,os:n,device:r}=l(e);Object.assign(_,{browser:t,os:n,device:r,screen:c(),location:i(),referrer:document.referrer})},S=async()=>{if(!h||b)return;let[i,a,o,s]=await Promise.all([e(),t(),n(),r()]);b||Object.assign(_,{isIncognito:i,hasCamera:a.hasCamera,hasMicrophone:a.hasMicrophone,hasBattery:o.hasBattery,batteryLevel:o.level,batteryCharging:o.charging,permissions:s})},C=()=>{typeof navigator>`u`||typeof window>`u`||Object.assign(_,{network:a(),performance:o(),pageVisibility:document.visibilityState===`visible`?`visible`:`hidden`})},w=()=>{if(!h)return;let e=()=>{S()};if(g){s(e);return}e()};if(m&&typeof document<`u`){let e=()=>{_.pageVisibility=document.visibilityState===`visible`?`visible`:`hidden`};document.addEventListener(`visibilitychange`,e),v=e}return f&&typeof window<`u`&&(y=setInterval(C,p)),x(),C(),w(),{data:_,refresh:async()=>{x(),C(),await S()},destroy:()=>{b=!0,y!==null&&(clearInterval(y),y=null),v&&typeof document<`u`&&(document.removeEventListener(`visibilitychange`,v),v=null)},get isMobile(){return _.device.isMobile},get isTablet(){return _.device.isTablet},get isDesktop(){return _.device.isDesktop},get isOnline(){return _.network.online},get isVisible(){return _.pageVisibility===`visible`},get browserName(){return _.browser.name},get osName(){return _.os.name}}}exports.useSoloAnalytics=d;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/utils/features.ts","../src/utils/location.ts","../src/utils/network.ts","../src/utils/performance.ts","../src/utils/scheduleIdle.ts","../src/utils/screen.ts","../src/utils/userAgent.ts","../src/useSoloAnalytics.ts"],"sourcesContent":["export async function detectIncognito(): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n if (typeof window === \"undefined\" || typeof indexedDB === \"undefined\") {\n resolve(false);\n return;\n }\n\n let settled = false;\n const finish = (value: boolean): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n resolve(value);\n };\n\n const timeoutId = setTimeout(() => finish(false), 1000);\n\n const db = indexedDB.open(\"test\");\n db.onerror = () => finish(true);\n db.onsuccess = () => {\n db.result?.close();\n finish(false);\n };\n });\n}\n\nexport async function checkMediaCapabilities(): Promise<{\n hasCamera: boolean | null;\n hasMicrophone: boolean | null;\n}> {\n if (\n typeof navigator === \"undefined\" ||\n !navigator.mediaDevices ||\n !navigator.mediaDevices.enumerateDevices\n ) {\n return { hasCamera: null, hasMicrophone: null };\n }\n\n try {\n const devices = await navigator.mediaDevices.enumerateDevices();\n const hasCamera = devices.some((device) => device.kind === \"videoinput\");\n const hasMicrophone = devices.some((device) => device.kind === \"audioinput\");\n\n return { hasCamera, hasMicrophone };\n } catch {\n return { hasCamera: null, hasMicrophone: null };\n }\n}\n\nexport async function getBatteryInfo(): Promise<{\n hasBattery: boolean | null;\n level: number | null;\n charging: boolean | null;\n}> {\n // @ts-expect-error: getBattery method is non-standard\n if (typeof navigator === \"undefined\" || !navigator.getBattery) {\n return { hasBattery: null, level: null, charging: null };\n }\n\n try {\n // @ts-expect-error\n const battery = await navigator.getBattery();\n return {\n hasBattery: true,\n level: battery.level,\n charging: battery.charging,\n };\n } catch {\n return { hasBattery: null, level: null, charging: null };\n }\n}\n\nexport async function checkPermissions(): Promise<Record<string, string>> {\n if (typeof navigator === \"undefined\" || !navigator.permissions || !navigator.permissions.query) {\n return {};\n }\n\n const permissions: Record<string, string> = {};\n const featuresToCheck = [\n \"geolocation\",\n \"notifications\",\n \"push\",\n \"midi\",\n \"camera\",\n \"microphone\",\n \"background-sync\",\n \"accelerometer\",\n \"gyroscope\",\n \"magnetometer\",\n ];\n\n const results = await Promise.all(\n featuresToCheck.map(async (feature) => {\n try {\n const result = await navigator.permissions!.query({ name: feature as PermissionName });\n return [feature, result.state] as const;\n } catch {\n return [feature, \"not-supported\"] as const;\n }\n }),\n );\n\n for (const [feature, state] of results) {\n permissions[feature] = state;\n }\n\n return permissions;\n}\n","import type { LocationInfo } from \"../types/soloAnalytics\";\n\nexport function getLocationInfo(): LocationInfo {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n return {\n timeZone: \"\",\n language: \"\",\n languages: [],\n isRestricted: false,\n doNotTrack: null,\n cookiesEnabled: false,\n localStorage: false,\n sessionStorage: false,\n };\n }\n\n // Get timezone\n const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n\n // Get language info\n const language = navigator.language || \"\";\n const languages = navigator.languages ? Array.from(navigator.languages) : [language];\n\n // Check if window is restricted (e.g., iframe with restricted access)\n let isRestricted = false;\n try {\n isRestricted = window.self !== window.top;\n } catch {\n isRestricted = true;\n }\n\n // Check Do Not Track setting\n let doNotTrack = null;\n if (navigator.doNotTrack === \"1\" || navigator.doNotTrack === \"yes\") {\n doNotTrack = true;\n } else if (navigator.doNotTrack === \"0\" || navigator.doNotTrack === \"no\") {\n doNotTrack = false;\n }\n\n // Check if cookies are enabled\n const cookiesEnabled = navigator.cookieEnabled;\n\n // Check for storage availability\n const checkStorage = (type: \"localStorage\" | \"sessionStorage\"): boolean => {\n try {\n const storage = window[type];\n const testKey = `__test_${type}__`;\n storage.setItem(testKey, \"test\");\n storage.removeItem(testKey);\n return true;\n } catch {\n return false;\n }\n };\n\n return {\n timeZone,\n language,\n languages,\n isRestricted,\n doNotTrack,\n cookiesEnabled,\n localStorage: checkStorage(\"localStorage\"),\n sessionStorage: checkStorage(\"sessionStorage\"),\n };\n}\n","import type { NetworkInfo } from \"../types/soloAnalytics\";\n\nexport function getNetworkInfo(): NetworkInfo {\n const online = typeof navigator !== \"undefined\" ? navigator.onLine : false;\n\n // Default values\n let effectiveType = \"unknown\";\n let downlink = 0;\n let rtt = 0;\n let saveData = false;\n\n // NetworkInformation API (limited browser support)\n // @ts-expect-error: connection property is non-standard\n if (navigator?.connection) {\n // @ts-expect-error\n const connection = navigator.connection;\n\n effectiveType = connection.effectiveType || effectiveType;\n downlink = connection.downlink || downlink;\n rtt = connection.rtt || rtt;\n saveData = connection.saveData || saveData;\n }\n\n return {\n online,\n effectiveType,\n downlink,\n rtt,\n saveData,\n };\n}\n","import type { PerformanceInfo } from \"../types/soloAnalytics\";\n\nexport function getPerformanceInfo(): PerformanceInfo {\n if (typeof window === \"undefined\") {\n return {\n memory: null,\n navigation: { type: \"Unknown\", redirectCount: 0 },\n timing: {\n loadTime: 0,\n domContentLoaded: 0,\n firstPaint: null,\n firstContentfulPaint: null,\n },\n };\n }\n\n const getNavigationInfo = () => {\n if (!window.performance?.navigation) {\n return { type: \"Unknown\", redirectCount: 0 };\n }\n\n const navTypes = [\"navigate\", \"reload\", \"back_forward\", \"prerender\"];\n const navType = navTypes[window.performance.navigation.type] || \"Unknown\";\n\n return {\n type: navType,\n redirectCount: window.performance.navigation.redirectCount,\n };\n };\n\n const getTimingInfo = () => {\n if (!window.performance?.timing) {\n return {\n loadTime: 0,\n domContentLoaded: 0,\n firstPaint: null,\n firstContentfulPaint: null,\n };\n }\n\n const timing = window.performance.timing;\n const loadTime = timing.loadEventEnd - timing.navigationStart;\n const domContentLoaded = timing.domContentLoadedEventEnd - timing.navigationStart;\n\n // Get first paint and first contentful paint\n let firstPaint = null;\n let firstContentfulPaint = null;\n\n if (window.performance && typeof window.performance.getEntriesByType === \"function\") {\n const paintMetrics = window.performance.getEntriesByType(\"paint\");\n\n const fp = paintMetrics.find((entry) => entry.name === \"first-paint\");\n const fcp = paintMetrics.find((entry) => entry.name === \"first-contentful-paint\");\n\n if (fp) firstPaint = fp.startTime;\n if (fcp) firstContentfulPaint = fcp.startTime;\n }\n\n return {\n loadTime,\n domContentLoaded,\n firstPaint,\n firstContentfulPaint,\n };\n };\n\n const getMemoryInfo = () => {\n // @ts-expect-error: performance.memory is non-standard (Chrome only)\n if (window.performance?.memory) {\n // @ts-expect-error\n const memory = window.performance.memory;\n return {\n jsHeapSizeLimit: memory.jsHeapSizeLimit,\n totalJSHeapSize: memory.totalJSHeapSize,\n usedJSHeapSize: memory.usedJSHeapSize,\n };\n }\n return null;\n };\n\n return {\n memory: getMemoryInfo(),\n navigation: getNavigationInfo(),\n timing: getTimingInfo(),\n };\n}\n","/**\n * Schedules work during browser idle time to avoid blocking the main thread.\n * Falls back to setTimeout when requestIdleCallback is unavailable.\n */\nexport function scheduleIdle(callback: () => void | Promise<void>, timeout = 2000): void {\n if (typeof window === \"undefined\") {\n void Promise.resolve().then(callback);\n return;\n }\n\n const run = (): void => {\n void Promise.resolve(callback());\n };\n\n if (\"requestIdleCallback\" in window) {\n window.requestIdleCallback(() => run(), { timeout });\n return;\n }\n\n setTimeout(run, 0);\n}\n","import type { ScreenInfo } from \"../types/soloAnalytics\";\n\nexport function getScreenInfo(): ScreenInfo {\n if (typeof window === \"undefined\" || typeof screen === \"undefined\") {\n return {\n width: 0,\n height: 0,\n availWidth: 0,\n availHeight: 0,\n colorDepth: 0,\n orientation: \"unknown\",\n pixelRatio: 1,\n touchPoints: 0,\n };\n }\n\n // Get screen orientation\n let orientation = \"unknown\";\n if (window.innerHeight > window.innerWidth) {\n orientation = \"portrait\";\n } else {\n orientation = \"landscape\";\n }\n\n // Try to get more precise orientation if available\n if (screen.orientation?.type) {\n orientation = screen.orientation.type;\n }\n\n // Get pixel ratio\n const pixelRatio = window.devicePixelRatio || 1;\n\n // Get touch points\n const touchPoints = navigator.maxTouchPoints || 0;\n\n return {\n width: screen.width,\n height: screen.height,\n availWidth: screen.availWidth,\n availHeight: screen.availHeight,\n colorDepth: screen.colorDepth,\n orientation,\n pixelRatio,\n touchPoints,\n };\n}\n","import type { BrowserInfo, DeviceInfo, OSInfo } from \"../types/soloAnalytics\";\n\nexport interface UserAgentOptions {\n vendor?: string;\n innerWidth?: number;\n innerHeight?: number;\n maxTouchPoints?: number;\n}\n\nexport function parseUserAgent(\n ua: string,\n options: UserAgentOptions = {},\n): {\n browser: BrowserInfo;\n os: OSInfo;\n device: DeviceInfo;\n} {\n const vendor = options.vendor ?? (typeof navigator !== \"undefined\" ? navigator.vendor : \"\");\n const innerWidth =\n options.innerWidth ?? (typeof window !== \"undefined\" ? window.innerWidth : 1024);\n const innerHeight =\n options.innerHeight ?? (typeof window !== \"undefined\" ? window.innerHeight : 768);\n const maxTouchPoints =\n options.maxTouchPoints ??\n (typeof navigator !== \"undefined\" && \"maxTouchPoints\" in navigator\n ? navigator.maxTouchPoints\n : 0);\n\n // Browser detection\n const getBrowser = (): BrowserInfo => {\n const browsers = [\n { name: \"Edge\", regex: /Edg(?:e|A|iOS)?\\/([0-9.]+)/ },\n { name: \"Samsung Browser\", regex: /SamsungBrowser\\/([0-9.]+)/ },\n { name: \"Opera\", regex: /(?:Opera|OPR)\\/([0-9.]+)/ },\n { name: \"Firefox\", regex: /Firefox\\/([0-9.]+)/ },\n { name: \"Chrome\", regex: /Chrome\\/([0-9.]+)/ },\n { name: \"Safari\", regex: /Version\\/([0-9.]+).*Safari/ },\n { name: \"IE\", regex: /MSIE|Trident/ },\n ];\n\n for (const browser of browsers) {\n const match = ua.match(browser.regex);\n if (match) {\n const version = match[1] || \"\";\n const major = version.split(\".\")[0] || \"\";\n\n // Engine detection\n let engine = \"Unknown\";\n let engineVersion = \"\";\n\n if (ua.includes(\"AppleWebKit\")) {\n const webkitMatch = ua.match(/AppleWebKit\\/([0-9.]+)/);\n engine = \"WebKit\";\n engineVersion = webkitMatch ? webkitMatch[1] : \"\";\n } else if (ua.includes(\"Gecko\")) {\n engine = \"Gecko\";\n const geckoMatch = ua.match(/rv:([0-9.]+)/);\n engineVersion = geckoMatch ? geckoMatch[1] : \"\";\n } else if (ua.includes(\"Trident\")) {\n engine = \"Trident\";\n const tridentMatch = ua.match(/Trident\\/([0-9.]+)/);\n engineVersion = tridentMatch ? tridentMatch[1] : \"\";\n }\n\n return {\n name: browser.name,\n version,\n major,\n userAgent: ua,\n vendor,\n engine,\n engineVersion,\n };\n }\n }\n\n return {\n name: \"Unknown\",\n version: \"\",\n major: \"\",\n userAgent: ua,\n vendor,\n engine: \"Unknown\",\n engineVersion: \"\",\n };\n };\n\n // OS detection\n const getOS = (): OSInfo => {\n const osMatchers = [\n { name: \"iOS\", regex: /iPhone|iPad|iPod/ },\n { name: \"Android\", regex: /Android ([0-9.]+)/ },\n { name: \"Windows\", regex: /Windows NT ([0-9.]+)/ },\n { name: \"macOS\", regex: /Mac OS X ([0-9_.]+)/ },\n { name: \"Linux\", regex: /Linux/ },\n ];\n\n for (const os of osMatchers) {\n const match = ua.match(os.regex);\n if (match) {\n let version = \"\";\n if (match[1]) {\n version = os.name === \"macOS\" ? match[1].replace(/_/g, \".\") : match[1];\n }\n\n return {\n name: os.name,\n version,\n architecture: ua.includes(\"x64\") || ua.includes(\"x86_64\") ? \"64-bit\" : \"32-bit\",\n };\n }\n }\n\n return {\n name: \"Unknown\",\n version: \"\",\n architecture: ua.includes(\"x64\") || ua.includes(\"x86_64\") ? \"64-bit\" : \"32-bit\",\n };\n };\n\n // Device detection\n const getDevice = (): DeviceInfo => {\n const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(\n ua,\n );\n const isTablet = /iPad|Android(?!.*Mobile)/i.test(ua);\n\n let vendor = \"Unknown\";\n let model = \"Unknown\";\n\n if (ua.includes(\"iPhone\") || ua.includes(\"iPad\") || ua.includes(\"iPod\")) {\n vendor = \"Apple\";\n if (ua.includes(\"iPhone\")) model = \"iPhone\";\n if (ua.includes(\"iPad\")) model = \"iPad\";\n if (ua.includes(\"iPod\")) model = \"iPod\";\n } else if (ua.includes(\"Samsung\")) {\n vendor = \"Samsung\";\n } else if (ua.includes(\"Pixel\")) {\n vendor = \"Google\";\n const pixelMatch = ua.match(/Pixel ([0-9XL]+)/);\n if (pixelMatch) model = `Pixel ${pixelMatch[1]}`;\n }\n\n const deviceType = isTablet ? \"tablet\" : isMobileDevice ? \"mobile\" : \"desktop\";\n\n return {\n type: deviceType,\n vendor,\n model,\n orientation: innerHeight > innerWidth ? \"portrait\" : \"landscape\",\n isMobile: deviceType === \"mobile\",\n isTablet: deviceType === \"tablet\",\n isDesktop: deviceType === \"desktop\",\n touch: maxTouchPoints > 0,\n };\n };\n\n return {\n browser: getBrowser(),\n os: getOS(),\n device: getDevice(),\n };\n}\n","import type { SoloAnalyticsInfo } from \"./types/soloAnalytics\";\nimport {\n checkMediaCapabilities,\n checkPermissions,\n detectIncognito,\n getBatteryInfo,\n} from \"./utils/features\";\nimport { getLocationInfo } from \"./utils/location\";\nimport { getNetworkInfo } from \"./utils/network\";\nimport { getPerformanceInfo } from \"./utils/performance\";\nimport { scheduleIdle } from \"./utils/scheduleIdle\";\nimport { getScreenInfo } from \"./utils/screen\";\nimport { parseUserAgent } from \"./utils/userAgent\";\n\nexport interface SoloAnalyticsOptions {\n /** Automatically refresh dynamic data (network, performance) */\n autoRefresh?: boolean;\n /** Refresh interval in milliseconds (default: 30000) */\n refreshInterval?: number;\n /** Track page visibility changes */\n trackVisibility?: boolean;\n /**\n * Run expensive async probes (incognito, battery, media, permissions).\n * Set to `false` to skip entirely and reduce main-thread work.\n * @default true\n */\n detectFeatures?: boolean;\n /**\n * Defer async feature probes to idle time via requestIdleCallback.\n * Sync data (UA, screen, network) is collected immediately.\n * @default true\n */\n lazyFeatures?: boolean;\n}\n\nexport interface SoloAnalyticsReturn {\n data: SoloAnalyticsInfo;\n refresh: () => Promise<void>;\n isMobile: boolean;\n isTablet: boolean;\n isDesktop: boolean;\n isOnline: boolean;\n isVisible: boolean;\n browserName: string;\n osName: string;\n destroy: () => void;\n}\n\nconst createInitialData = (): SoloAnalyticsInfo => ({\n browser: {\n name: \"\",\n version: \"\",\n major: \"\",\n userAgent: \"\",\n vendor: \"\",\n engine: \"\",\n engineVersion: \"\",\n },\n os: {\n name: \"\",\n version: \"\",\n architecture: \"\",\n },\n device: {\n type: \"unknown\",\n vendor: \"\",\n model: \"\",\n orientation: \"portrait\",\n isMobile: false,\n isTablet: false,\n isDesktop: false,\n touch: false,\n },\n network: {\n online: false,\n effectiveType: \"\",\n downlink: 0,\n rtt: 0,\n saveData: false,\n },\n screen: {\n width: 0,\n height: 0,\n availWidth: 0,\n availHeight: 0,\n colorDepth: 0,\n orientation: \"\",\n pixelRatio: 1,\n touchPoints: 0,\n },\n performance: {\n memory: null,\n navigation: {\n type: \"\",\n redirectCount: 0,\n },\n timing: {\n loadTime: 0,\n domContentLoaded: 0,\n firstPaint: null,\n firstContentfulPaint: null,\n },\n },\n location: {\n timeZone: \"\",\n language: \"\",\n languages: [],\n isRestricted: false,\n doNotTrack: null,\n cookiesEnabled: false,\n localStorage: false,\n sessionStorage: false,\n },\n pageVisibility: \"visible\",\n referrer: \"\",\n isIncognito: false,\n hasCamera: null,\n hasMicrophone: null,\n hasBattery: null,\n batteryLevel: null,\n batteryCharging: null,\n permissions: {},\n});\n\nexport function useSoloAnalytics(options: SoloAnalyticsOptions = {}): SoloAnalyticsReturn {\n const {\n autoRefresh = false,\n refreshInterval = 30000,\n trackVisibility = true,\n detectFeatures = true,\n lazyFeatures = true,\n } = options;\n\n const analyticsData = createInitialData();\n\n let visibilityChangeListener: (() => void) | null = null;\n let refreshTimer: ReturnType<typeof setInterval> | null = null;\n let destroyed = false;\n\n const collectSyncData = (): void => {\n if (typeof navigator === \"undefined\" || typeof window === \"undefined\") {\n return;\n }\n\n const userAgent = navigator.userAgent;\n const { browser, os, device } = parseUserAgent(userAgent);\n\n Object.assign(analyticsData, {\n browser,\n os,\n device,\n screen: getScreenInfo(),\n location: getLocationInfo(),\n referrer: document.referrer,\n });\n };\n\n const collectAsyncFeatures = async (): Promise<void> => {\n if (!detectFeatures || destroyed) {\n return;\n }\n\n const [isIncognito, media, battery, permissions] = await Promise.all([\n detectIncognito(),\n checkMediaCapabilities(),\n getBatteryInfo(),\n checkPermissions(),\n ]);\n\n if (destroyed) {\n return;\n }\n\n Object.assign(analyticsData, {\n isIncognito,\n hasCamera: media.hasCamera,\n hasMicrophone: media.hasMicrophone,\n hasBattery: battery.hasBattery,\n batteryLevel: battery.level,\n batteryCharging: battery.charging,\n permissions,\n });\n };\n\n const collectDynamicData = (): void => {\n if (typeof navigator === \"undefined\" || typeof window === \"undefined\") {\n return;\n }\n\n Object.assign(analyticsData, {\n network: getNetworkInfo(),\n performance: getPerformanceInfo(),\n pageVisibility: document.visibilityState === \"visible\" ? \"visible\" : \"hidden\",\n });\n };\n\n const runFeatureDetection = (): void => {\n if (!detectFeatures) {\n return;\n }\n\n const run = (): void => {\n void collectAsyncFeatures();\n };\n\n if (lazyFeatures) {\n scheduleIdle(run);\n return;\n }\n\n run();\n };\n\n if (trackVisibility && typeof document !== \"undefined\") {\n const handleVisibilityChange = (): void => {\n analyticsData.pageVisibility = document.visibilityState === \"visible\" ? \"visible\" : \"hidden\";\n };\n\n document.addEventListener(\"visibilitychange\", handleVisibilityChange);\n visibilityChangeListener = handleVisibilityChange;\n }\n\n if (autoRefresh && typeof window !== \"undefined\") {\n refreshTimer = setInterval(collectDynamicData, refreshInterval);\n }\n\n const init = (): void => {\n collectSyncData();\n collectDynamicData();\n runFeatureDetection();\n };\n\n init();\n\n const refresh = async (): Promise<void> => {\n collectSyncData();\n collectDynamicData();\n await collectAsyncFeatures();\n };\n\n const destroy = (): void => {\n destroyed = true;\n\n if (refreshTimer !== null) {\n clearInterval(refreshTimer);\n refreshTimer = null;\n }\n\n if (visibilityChangeListener && typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", visibilityChangeListener);\n visibilityChangeListener = null;\n }\n };\n\n return {\n data: analyticsData,\n refresh,\n destroy,\n get isMobile(): boolean {\n return analyticsData.device.isMobile;\n },\n get isTablet(): boolean {\n return analyticsData.device.isTablet;\n },\n get isDesktop(): boolean {\n return analyticsData.device.isDesktop;\n },\n get isOnline(): boolean {\n return analyticsData.network.online;\n },\n get isVisible(): boolean {\n return analyticsData.pageVisibility === \"visible\";\n },\n get browserName(): string {\n return analyticsData.browser.name;\n },\n get osName(): string {\n return analyticsData.os.name;\n },\n };\n}\n"],"mappings":"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"}