solo-analytics 0.2.0 → 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,418 +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
- // Skip expensive async probes (incognito, battery, media, permissions)
58
- detectFeatures: true,
59
- // Defer async probes to idle time (default: true, recommended)
60
- lazyFeatures: true,
61
- });
62
- ```
63
-
64
- ## API Reference
9
+ ## Why this exists
65
10
 
66
- 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.
67
12
 
68
- ### Methods
13
+ Use it when you need to:
69
14
 
70
- - `refresh()`: Manually triggers a refresh of all analytics data
71
- - `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
72
18
 
73
- ### 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.
74
20
 
75
- - `isMobile`: Whether the device is a mobile phone
76
- - `isTablet`: Whether the device is a tablet
77
- - `isDesktop`: Whether the device is a desktop computer
78
- - `isOnline`: Current network connection state
79
- - `isVisible`: Whether the page is currently visible
80
- - `browserName`: The name of the browser
81
- - `osName`: The name of the operating system
21
+ ## What you get
82
22
 
83
- ### 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 |
84
31
 
85
- - `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.
86
33
 
87
- ## Data Structure
88
-
89
- The `data` object contains the following structure:
34
+ ## Usage
90
35
 
91
36
  ```typescript
92
- interface SoloAnalyticsInfo {
93
- browser: {
94
- name: string;
95
- version: string;
96
- major: string;
97
- userAgent: string;
98
- vendor: string;
99
- engine: string;
100
- engineVersion: string;
101
- };
102
- os: {
103
- name: string;
104
- version: string;
105
- architecture: string;
106
- };
107
- device: {
108
- type: "mobile" | "tablet" | "desktop" | "unknown";
109
- vendor: string;
110
- model: string;
111
- orientation: "portrait" | "landscape";
112
- isMobile: boolean;
113
- isTablet: boolean;
114
- isDesktop: boolean;
115
- touch: boolean;
116
- };
117
- network: {
118
- online: boolean;
119
- effectiveType: string;
120
- downlink: number;
121
- rtt: number;
122
- saveData: boolean;
123
- };
124
- screen: {
125
- width: number;
126
- height: number;
127
- availWidth: number;
128
- availHeight: number;
129
- colorDepth: number;
130
- orientation: string;
131
- pixelRatio: number;
132
- touchPoints: number;
133
- };
134
- performance: {
135
- memory: {
136
- jsHeapSizeLimit: number;
137
- totalJSHeapSize: number;
138
- usedJSHeapSize: number;
139
- } | null;
140
- navigation: {
141
- type: string;
142
- redirectCount: number;
143
- };
144
- timing: {
145
- loadTime: number;
146
- domContentLoaded: number;
147
- firstPaint: number | null;
148
- firstContentfulPaint: number | null;
149
- };
150
- };
151
- location: {
152
- timeZone: string;
153
- language: string;
154
- languages: string[];
155
- isRestricted: boolean;
156
- doNotTrack: boolean | null;
157
- cookiesEnabled: boolean;
158
- localStorage: boolean;
159
- sessionStorage: boolean;
160
- };
161
- pageVisibility: "visible" | "hidden";
162
- referrer: string;
163
- isIncognito: boolean;
164
- hasCamera: boolean | null;
165
- hasMicrophone: boolean | null;
166
- hasBattery: boolean | null;
167
- batteryLevel: number | null;
168
- batteryCharging: boolean | null;
169
- permissions: Record<string, string>;
170
- }
171
- ```
172
-
173
- ## Framework Integration Examples
174
-
175
- ### React
176
-
177
- ```tsx
178
- import { useEffect, useState } from "react";
179
- import { useSoloAnalytics, SoloAnalyticsReturn } from "solo-analytics";
180
-
181
- function DeviceInfo() {
182
- const [analytics, setAnalytics] = useState<SoloAnalyticsReturn | null>(null);
183
-
184
- useEffect(() => {
185
- // Initialize on the client side
186
- const analyticsInstance = useSoloAnalytics();
187
- setAnalytics(analyticsInstance);
188
-
189
- // Cleanup on unmount
190
- return () => {
191
- analyticsInstance.destroy();
192
- };
193
- }, []);
194
-
195
- if (!analytics) return <div>Loading...</div>;
196
-
197
- return (
198
- <div>
199
- <h1>Device Information</h1>
200
- <p>
201
- Type:{" "}
202
- {analytics.isMobile
203
- ? "Mobile"
204
- : analytics.isTablet
205
- ? "Tablet"
206
- : "Desktop"}
207
- </p>
208
- <p>Browser: {analytics.browserName}</p>
209
- <p>Operating System: {analytics.osName}</p>
210
- <p>Network Status: {analytics.isOnline ? "Online" : "Offline"}</p>
211
- </div>
212
- );
213
- }
214
- ```
215
-
216
- ### Vue 3
217
-
218
- ```vue
219
- <template>
220
- <div>
221
- <h1>Device Information</h1>
222
- <p>Type: {{ deviceType }}</p>
223
- <p>Browser: {{ analytics.browserName }}</p>
224
- <p>Operating System: {{ analytics.osName }}</p>
225
- <p>Network Status: {{ analytics.isOnline ? "Online" : "Offline" }}</p>
226
- </div>
227
- </template>
228
-
229
- <script setup lang="ts">
230
- import { onMounted, onUnmounted, ref, computed } from "vue";
231
- import { useSoloAnalytics, SoloAnalyticsReturn } from "solo-analytics";
232
-
233
- const analytics = ref<SoloAnalyticsReturn | null>(null);
234
-
235
- onMounted(() => {
236
- analytics.value = useSoloAnalytics({
237
- trackVisibility: true,
238
- });
239
- });
37
+ import { useSoloAnalytics } from "solo-analytics";
240
38
 
241
- onUnmounted(() => {
242
- analytics.value?.destroy();
243
- });
39
+ const analytics = useSoloAnalytics();
244
40
 
245
- const deviceType = computed(() => {
246
- if (!analytics.value) return "Unknown";
41
+ console.log(analytics.isMobile);
42
+ console.log(analytics.browserName);
43
+ console.log(analytics.data);
247
44
 
248
- if (analytics.value.isMobile) return "Mobile";
249
- if (analytics.value.isTablet) return "Tablet";
250
- return "Desktop";
251
- });
252
- </script>
45
+ // When done (SPA unmount, etc.)
46
+ analytics.destroy();
253
47
  ```
254
48
 
255
- ### Nuxt 3
256
-
257
- ```vue
258
- <template>
259
- <div>
260
- <h1>Device Information</h1>
261
- <div v-if="analytics">
262
- <p>Type: {{ deviceType }}</p>
263
- <p>Browser: {{ analytics.browserName }}</p>
264
- <p>Operating System: {{ analytics.osName }}</p>
265
- <p>Network Status: {{ analytics.isOnline ? "Online" : "Offline" }}</p>
266
- </div>
267
- <div v-else>
268
- <p>Loading device information...</p>
269
- </div>
270
- </div>
271
- </template>
272
-
273
- <script setup lang="ts">
274
- import { ref, computed } from "vue";
275
- import { useSoloAnalytics, SoloAnalyticsReturn } from "solo-analytics";
276
-
277
- const analytics = ref<SoloAnalyticsReturn | null>(null);
278
-
279
- // Important: Only initialize on client-side to avoid SSR issues
280
- onMounted(() => {
281
- analytics.value = useSoloAnalytics({
282
- trackVisibility: true,
283
- autoRefresh: true,
284
- refreshInterval: 15000,
285
- });
286
- });
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.
287
50
 
288
- onBeforeUnmount(() => {
289
- if (analytics.value) {
290
- analytics.value.destroy();
291
- }
292
- });
293
-
294
- const deviceType = computed(() => {
295
- if (!analytics.value) return "Unknown";
51
+ ### Options
296
52
 
297
- if (analytics.value.isMobile) return "Mobile";
298
- if (analytics.value.isTablet) return "Tablet";
299
- 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)
300
60
  });
301
- </script>
302
61
  ```
303
62
 
304
- ### Vanilla JavaScript
63
+ Set `detectFeatures: false` if you only need UA, screen, and network data. That skips the async work entirely.
305
64
 
306
- ```html
307
- <script type="module">
308
- import { useSoloAnalytics } from "solo-analytics";
65
+ ### API
309
66
 
310
- document.addEventListener("DOMContentLoaded", () => {
311
- 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 |
312
76
 
313
- // Display information on the page
314
- document.getElementById("deviceType").textContent = analytics.isMobile
315
- ? "Mobile"
316
- : analytics.isTablet
317
- ? "Tablet"
318
- : "Desktop";
77
+ Types are exported: `SoloAnalyticsInfo`, `SoloAnalyticsOptions`, `SoloAnalyticsReturn`, and per-section interfaces.
319
78
 
320
- document.getElementById(
321
- "browserInfo"
322
- ).textContent = `${analytics.browserName} on ${analytics.osName}`;
79
+ ### SSR
323
80
 
324
- // Cleanup on window close
325
- window.addEventListener("beforeunload", () => {
326
- analytics.destroy();
327
- });
328
- });
329
- </script>
330
- ```
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.
331
82
 
332
- ## Browser Compatibility
83
+ ### Limitations
333
84
 
334
- Solo Analytics is compatible with all modern browsers, including:
85
+ Be aware of what this library can and cannot guarantee:
335
86
 
336
- - Chrome 60+
337
- - Firefox 55+
338
- - Safari 11+
339
- - Edge 16+
340
- - Opera 47+
341
- - iOS Safari 11+
342
- - Android Browser 67+
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. |
343
95
 
344
- Some advanced features may not be available in older browsers but the library includes graceful fallbacks.
96
+ ## Bundle size
97
+
98
+ Roughly 3–4 KB gzipped. `sideEffects: false` in `package.json` so bundlers can tree-shake unused exports.
345
99
 
346
100
  ## Development
347
101
 
102
+ This project uses [Vite+](https://viteplus.dev/) (Vite, Vitest, Oxlint, Oxfmt, tsdown) via the `vp` CLI.
103
+
348
104
  ```bash
349
- # Install dependencies
350
105
  npm install
351
-
352
- # Run development server (demo playground)
353
- npm run dev
354
-
355
- # Type-check and build for production
356
- npm run build
357
-
358
- # Run tests
359
- npm test
360
-
361
- # Watch tests during development
362
- npm run test:watch
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
363
112
  ```
364
113
 
365
- ## Performance
366
-
367
- Solo Analytics is designed to stay off the critical path:
368
-
369
- | Metric | Value |
370
- |--------|-------|
371
- | ESM bundle (gzip) | ~3.5 KB |
372
- | UMD bundle (gzip) | ~3.1 KB |
373
- | `sideEffects` | `false` (tree-shakeable) |
374
-
375
- **Recommendations:**
376
-
377
- - Use `lazyFeatures: true` (default) so async probes run during idle time
378
- - Set `detectFeatures: false` if you only need UA, screen, and network data
379
- - Call `destroy()` when unmounting in SPAs to clear timers and listeners
380
- - Initialize client-side only (`onMounted` in Vue/Nuxt, `useEffect` in React)
381
-
382
114
  ## Contributing
383
115
 
384
- Contributions are welcome! Please feel free to submit a Pull Request.
385
-
386
- 1. Fork the repository
387
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
388
- 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
389
- 4. Push to the branch (`git push origin feature/amazing-feature`)
390
- 5. Open a Pull Request
391
-
392
- ## Repository
393
-
394
- 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).
395
117
 
396
118
  ## License
397
119
 
398
- MIT License
399
-
400
- Copyright (c) 2025 Céss White
401
-
402
- Permission is hereby granted, free of charge, to any person obtaining a copy
403
- of this software and associated documentation files (the "Software"), to deal
404
- in the Software without restriction, including without limitation the rights
405
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
406
- copies of the Software, and to permit persons to whom the Software is
407
- furnished to do so, subject to the following conditions:
408
-
409
- The above copyright notice and this permission notice shall be included in all
410
- copies or substantial portions of the Software.
411
-
412
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
413
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
414
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
415
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
416
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
417
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
418
- 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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"}