solo-analytics 0.2.0 → 0.3.2

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,320 @@
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.
3
+ **Read browser context once. Use it anywhere.**
4
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
5
+ Solo Analytics collects device, network, and environment data from the browser and returns it as a typed object. No backend, no vendor, no tracking — you decide what to do with the data.
23
6
 
24
7
  ```bash
25
8
  npm install solo-analytics
26
9
  ```
27
10
 
28
- ## Basic Usage
11
+ ---
12
+
13
+ ## Quick start
29
14
 
30
15
  ```typescript
31
16
  import { useSoloAnalytics } from "solo-analytics";
32
17
 
33
- // Initialize with default options
34
18
  const analytics = useSoloAnalytics();
35
19
 
36
- // Access common properties
37
- console.log("Mobile device:", analytics.isMobile);
38
- console.log("Browser:", analytics.browserName);
39
- console.log("OS:", analytics.osName);
20
+ // Shortcuts for common checks
21
+ console.log(analytics.isMobile); // true | false
22
+ console.log(analytics.browserName); // "Chrome", "Safari", "Firefox"…
23
+ console.log(analytics.osName); // "macOS", "Windows", "Android"…
24
+
25
+ // Full payload
26
+ console.log(analytics.data);
40
27
 
41
- // Access the complete data object
42
- console.log("Full analytics data:", analytics.data);
28
+ // Clean up when you're done (SPAs, component unmount)
29
+ analytics.destroy();
43
30
  ```
44
31
 
45
- ## Configuration Options
32
+ That is the entire API surface for most use cases. Everything below explains what you get, how to configure it, and how to integrate it in your stack.
46
33
 
47
- ```typescript
48
- import { useSoloAnalytics } from "solo-analytics";
34
+ ---
49
35
 
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
- });
36
+ ## What problem does this solve?
37
+
38
+ Most analytics SDKs are built to **send events to a vendor**. Solo Analytics does something different: it **reads context from the browser** and hands it back to you.
39
+
40
+ Typical uses:
41
+
42
+ | Goal | Example |
43
+ | ------------------------- | ------------------------------------------------------------------------------------------------------- |
44
+ | Enrich your own analytics | Attach `{ browser, device, network }` to every event you already send to PostHog, Mixpanel, or your API |
45
+ | Adapt the UI | Show a lite mode on slow connections (`effectiveType === "2g"`) or adjust layout for mobile |
46
+ | Debug client environments | One object instead of scattered `navigator` / `window` checks |
47
+
48
+ **What it is not:** a tracking product. It does not phone home, set cookies, or manage sessions.
49
+
50
+ ---
51
+
52
+ ## How it works
53
+
54
+ When you call `useSoloAnalytics()`, the library collects data in two phases:
55
+
56
+ ```
57
+ ┌─────────────────────────────────────────────────────────────┐
58
+ │ Phase 1 — Immediate (sync) │
59
+ │ User agent, screen, locale, network snapshot, performance │
60
+ └─────────────────────────────────────────────────────────────┘
61
+
62
+ ┌─────────────────────────────────────────────────────────────┐
63
+ │ Phase 2 — Deferred (async, on idle time by default) │
64
+ │ Incognito hint, battery, camera/mic, permission states │
65
+ └─────────────────────────────────────────────────────────────┘
62
66
  ```
63
67
 
64
- ## API Reference
68
+ - **Sync data** is available right after initialization.
69
+ - **Async probes** run when the browser is idle (`requestIdleCallback`) so they do not block rendering. Call `refresh()` if you need them immediately.
65
70
 
66
- The `useSoloAnalytics()` function returns an object with the following methods and properties:
71
+ Despite the `use` prefix (Vue convention), the function is **framework-agnostic**. Same API in React, Nuxt, Svelte, or plain JavaScript.
67
72
 
68
- ### Methods
73
+ ---
69
74
 
70
- - `refresh()`: Manually triggers a refresh of all analytics data
71
- - `destroy()`: Cleans up all event listeners and timers to prevent memory leaks
75
+ ## The data object
72
76
 
73
- ### Convenience Properties
77
+ `analytics.data` is a `SoloAnalyticsInfo` object. Here is what each section contains and when you might use it.
74
78
 
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
79
+ ### Browser, OS, and device
82
80
 
83
- ### Complete Data
81
+ Parsed from `navigator.userAgent`:
84
82
 
85
- - `data`: An object containing all detailed analytics information
83
+ ```typescript
84
+ analytics.data.browser; // name, version, engine, vendor…
85
+ analytics.data.os; // name, version, architecture
86
+ analytics.data.device; // type, isMobile, isTablet, isDesktop, touch…
87
+ ```
88
+
89
+ Shortcuts on the return value:
86
90
 
87
- ## Data Structure
91
+ ```typescript
92
+ analytics.isMobile; // analytics.data.device.isMobile
93
+ analytics.isTablet;
94
+ analytics.isDesktop;
95
+ analytics.browserName; // analytics.data.browser.name
96
+ analytics.osName; // analytics.data.os.name
97
+ ```
88
98
 
89
- The `data` object contains the following structure:
99
+ ### Screen
90
100
 
91
101
  ```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
- }
102
+ analytics.data.screen;
103
+ // width, height, availWidth, availHeight
104
+ // pixelRatio, colorDepth, orientation, touchPoints
171
105
  ```
172
106
 
173
- ## Framework Integration Examples
107
+ Useful for responsive decisions, screenshot tooling, or debugging layout issues reported by users.
174
108
 
175
- ### React
109
+ ### Network
176
110
 
177
- ```tsx
178
- import { useEffect, useState } from "react";
179
- import { useSoloAnalytics, SoloAnalyticsReturn } from "solo-analytics";
111
+ ```typescript
112
+ analytics.data.network;
113
+ // online, effectiveType ("4g", "3g", "slow-2g"…)
114
+ // downlink (Mbps), rtt (ms), saveData
115
+ ```
180
116
 
181
- function DeviceInfo() {
182
- const [analytics, setAnalytics] = useState<SoloAnalyticsReturn | null>(null);
117
+ Also exposed as `analytics.isOnline`. Enable `autoRefresh: true` if you need live updates when connection quality changes.
183
118
 
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
- }, []);
119
+ ### Performance
194
120
 
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
- }
121
+ ```typescript
122
+ analytics.data.performance;
123
+ // navigation type, load timing, first paint / FCP
124
+ // memory (Chrome only — null elsewhere)
214
125
  ```
215
126
 
216
- ### Vue 3
127
+ ### Environment and privacy signals
217
128
 
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>
129
+ ```typescript
130
+ analytics.data.location;
131
+ // timeZone, language, languages
132
+ // cookiesEnabled, localStorage, sessionStorage
133
+ // doNotTrack, isRestricted (iframe detection)
134
+ ```
228
135
 
229
- <script setup lang="ts">
230
- import { onMounted, onUnmounted, ref, computed } from "vue";
231
- import { useSoloAnalytics, SoloAnalyticsReturn } from "solo-analytics";
136
+ ### Optional async fields
232
137
 
233
- const analytics = ref<SoloAnalyticsReturn | null>(null);
138
+ Populated after idle-time probes (or immediately after `await analytics.refresh()`):
234
139
 
235
- onMounted(() => {
236
- analytics.value = useSoloAnalytics({
237
- trackVisibility: true,
238
- });
239
- });
140
+ ```typescript
141
+ analytics.data.isIncognito; // best-effort hint — see Limitations
142
+ analytics.data.hasCamera; // boolean | null
143
+ analytics.data.hasMicrophone; // boolean | null
144
+ analytics.data.hasBattery;
145
+ analytics.data.batteryLevel;
146
+ analytics.data.batteryCharging;
147
+ analytics.data.permissions; // Record<string, string>
148
+ ```
240
149
 
241
- onUnmounted(() => {
242
- analytics.value?.destroy();
150
+ Set `detectFeatures: false` to skip async probes entirely.
151
+
152
+ ---
153
+
154
+ ## Configuration
155
+
156
+ All options are optional. Defaults are sensible for most apps.
157
+
158
+ ```typescript
159
+ const analytics = useSoloAnalytics({
160
+ autoRefresh: false, // poll network + performance on an interval
161
+ refreshInterval: 30000, // ms between polls (only when autoRefresh: true)
162
+ trackVisibility: true, // update pageVisibility on visibilitychange
163
+ detectFeatures: true, // run async probes (incognito, battery, media…)
164
+ lazyFeatures: true, // defer async probes to idle time
243
165
  });
166
+ ```
167
+
168
+ | Option | Default | When to change it |
169
+ | ----------------- | ------- | -------------------------------------------------------------------------------------- |
170
+ | `autoRefresh` | `false` | Set `true` if network or performance data must stay current (e.g. connection-aware UI) |
171
+ | `refreshInterval` | `30000` | Lower for faster updates; higher to reduce work |
172
+ | `trackVisibility` | `true` | Set `false` if you never read `pageVisibility` or `isVisible` |
173
+ | `detectFeatures` | `true` | Set `false` for minimal bundle work — UA, screen, and network only |
174
+ | `lazyFeatures` | `true` | Set `false` if you need camera/battery/incognito data on first paint |
175
+
176
+ ### Minimal setup (fastest)
244
177
 
245
- const deviceType = computed(() => {
246
- if (!analytics.value) return "Unknown";
178
+ Only sync data no idle callbacks, no async probes:
247
179
 
248
- if (analytics.value.isMobile) return "Mobile";
249
- if (analytics.value.isTablet) return "Tablet";
250
- return "Desktop";
180
+ ```typescript
181
+ const analytics = useSoloAnalytics({
182
+ detectFeatures: false,
183
+ trackVisibility: false,
251
184
  });
252
- </script>
253
185
  ```
254
186
 
255
- ### Nuxt 3
187
+ ---
256
188
 
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>
189
+ ## Framework integration
190
+
191
+ Initialize **on the client only**. The library guards missing `window` / `navigator`; on the server, `data` stays at its initial empty state.
272
192
 
193
+ ### Vue / Nuxt
194
+
195
+ ```vue
273
196
  <script setup lang="ts">
274
- import { ref, computed } from "vue";
275
- import { useSoloAnalytics, SoloAnalyticsReturn } from "solo-analytics";
197
+ import { onMounted, onUnmounted, ref } from "vue";
198
+ import { useSoloAnalytics } from "solo-analytics";
276
199
 
277
- const analytics = ref<SoloAnalyticsReturn | null>(null);
200
+ const isMobile = ref(false);
201
+ let analytics: ReturnType<typeof useSoloAnalytics> | null = null;
278
202
 
279
- // Important: Only initialize on client-side to avoid SSR issues
280
203
  onMounted(() => {
281
- analytics.value = useSoloAnalytics({
282
- trackVisibility: true,
283
- autoRefresh: true,
284
- refreshInterval: 15000,
285
- });
204
+ analytics = useSoloAnalytics();
205
+ isMobile.value = analytics.isMobile;
286
206
  });
287
207
 
288
- onBeforeUnmount(() => {
289
- if (analytics.value) {
290
- analytics.value.destroy();
291
- }
292
- });
293
-
294
- const deviceType = computed(() => {
295
- if (!analytics.value) return "Unknown";
296
-
297
- if (analytics.value.isMobile) return "Mobile";
298
- if (analytics.value.isTablet) return "Tablet";
299
- return "Desktop";
208
+ onUnmounted(() => {
209
+ analytics?.destroy();
300
210
  });
301
211
  </script>
302
212
  ```
303
213
 
304
- ### Vanilla JavaScript
305
-
306
- ```html
307
- <script type="module">
308
- import { useSoloAnalytics } from "solo-analytics";
214
+ In Nuxt, keep initialization inside `onMounted` — not in `<script setup>` top level during SSR.
309
215
 
310
- document.addEventListener("DOMContentLoaded", () => {
311
- const analytics = useSoloAnalytics();
216
+ ### React
312
217
 
313
- // Display information on the page
314
- document.getElementById("deviceType").textContent = analytics.isMobile
315
- ? "Mobile"
316
- : analytics.isTablet
317
- ? "Tablet"
318
- : "Desktop";
319
-
320
- document.getElementById(
321
- "browserInfo"
322
- ).textContent = `${analytics.browserName} on ${analytics.osName}`;
323
-
324
- // Cleanup on window close
325
- window.addEventListener("beforeunload", () => {
326
- analytics.destroy();
327
- });
328
- });
329
- </script>
330
- ```
218
+ ```tsx
219
+ import { useEffect, useState } from "react";
220
+ import { useSoloAnalytics } from "solo-analytics";
331
221
 
332
- ## Browser Compatibility
222
+ function App() {
223
+ const [isMobile, setIsMobile] = useState(false);
333
224
 
334
- Solo Analytics is compatible with all modern browsers, including:
225
+ useEffect(() => {
226
+ const analytics = useSoloAnalytics();
227
+ setIsMobile(analytics.isMobile);
228
+ return () => analytics.destroy();
229
+ }, []);
335
230
 
336
- - Chrome 60+
337
- - Firefox 55+
338
- - Safari 11+
339
- - Edge 16+
340
- - Opera 47+
341
- - iOS Safari 11+
342
- - Android Browser 67+
231
+ return <div>{isMobile ? "Mobile" : "Desktop"}</div>;
232
+ }
233
+ ```
343
234
 
344
- Some advanced features may not be available in older browsers but the library includes graceful fallbacks.
235
+ ### Vanilla JS
345
236
 
346
- ## Development
237
+ See the [demo playground](src/demo/main.ts): call `useSoloAnalytics()`, read `analytics.data`, call `analytics.refresh()` on demand, and `analytics.destroy()` on `beforeunload`.
347
238
 
348
- ```bash
349
- # Install dependencies
350
- npm install
239
+ ---
351
240
 
352
- # Run development server (demo playground)
353
- npm run dev
241
+ ## API reference
354
242
 
355
- # Type-check and build for production
356
- npm run build
243
+ | Member | Type | Description |
244
+ | ------------- | ------------------- | ---------------------------------------- |
245
+ | `data` | `SoloAnalyticsInfo` | Full context object (updates in place) |
246
+ | `refresh()` | `Promise<void>` | Re-collect sync, dynamic, and async data |
247
+ | `destroy()` | `void` | Clear timers and event listeners |
248
+ | `isMobile` | `boolean` | Device class shortcut |
249
+ | `isTablet` | `boolean` | Device class shortcut |
250
+ | `isDesktop` | `boolean` | Device class shortcut |
251
+ | `isOnline` | `boolean` | `navigator.onLine` |
252
+ | `isVisible` | `boolean` | Page is currently visible |
253
+ | `browserName` | `string` | Parsed browser name |
254
+ | `osName` | `string` | Parsed OS name |
357
255
 
358
- # Run tests
359
- npm test
256
+ ### Exported types
360
257
 
361
- # Watch tests during development
362
- npm run test:watch
258
+ ```typescript
259
+ import type {
260
+ SoloAnalyticsInfo,
261
+ SoloAnalyticsOptions,
262
+ SoloAnalyticsReturn,
263
+ BrowserInfo,
264
+ DeviceInfo,
265
+ NetworkInfo,
266
+ ScreenInfo,
267
+ PerformanceInfo,
268
+ LocationInfo,
269
+ OSInfo,
270
+ } from "solo-analytics";
363
271
  ```
364
272
 
365
- ## Performance
273
+ ### Reactivity note
366
274
 
367
- Solo Analytics is designed to stay off the critical path:
275
+ `data` is a **mutable object** updated in place. There is no subscription API after async probes finish, call `refresh()` or read getters again. In Vue/React, copy values into reactive state if you need re-renders.
368
276
 
369
- | Metric | Value |
370
- |--------|-------|
371
- | ESM bundle (gzip) | ~3.5 KB |
372
- | UMD bundle (gzip) | ~3.1 KB |
373
- | `sideEffects` | `false` (tree-shakeable) |
277
+ ---
374
278
 
375
- **Recommendations:**
279
+ ## Limitations (read before relying on edge cases)
376
280
 
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)
281
+ | Topic | What to expect |
282
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
283
+ | **User-agent parsing** | Regex-based, not a full UA database. Edge cases exist (iPad desktop mode, new browsers, reduced UA strings). |
284
+ | **Client Hints** | `navigator.userAgentData` is not used yet. |
285
+ | **Performance memory** | `performance.memory` is Chrome-only; other browsers return `null`. |
286
+ | **Incognito detection** | Best-effort via IndexedDB — unreliable across browsers. Treat as a hint, not proof. |
287
+ | **Camera / microphone** | `enumerateDevices` may return empty until the user grants permission. `false` does not always mean hardware is absent. |
288
+ | **SSR** | Server-side calls produce empty defaults. Always init on the client. |
381
289
 
382
- ## Contributing
290
+ ---
383
291
 
384
- Contributions are welcome! Please feel free to submit a Pull Request.
292
+ ## Bundle size
385
293
 
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
294
+ Roughly **3–4 KB gzipped**. `sideEffects: false` in `package.json` bundlers can tree-shake unused exports.
391
295
 
392
- ## Repository
296
+ ---
393
297
 
394
- GitHub: [https://github.com/cesswhite/solo-analytics](https://github.com/cesswhite/solo-analytics)
298
+ ## Development
395
299
 
396
- ## License
300
+ This project uses [Vite+](https://viteplus.dev/) (`vp` CLI: Vite, Vitest, Oxlint, Oxfmt, tsdown).
397
301
 
398
- MIT License
302
+ ```bash
303
+ npm install
304
+ vp dev # local demo playground
305
+ vp pack # build library (ESM + CJS + types)
306
+ vp test # unit tests
307
+ vp test run --coverage
308
+ vp check # format, lint, type-check
309
+ vp check --fix # auto-fix format/lint
310
+ ```
399
311
 
400
- Copyright (c) 2025 Céss White
312
+ ---
401
313
 
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:
314
+ ## Contributing
408
315
 
409
- The above copyright notice and this permission notice shall be included in all
410
- copies or substantial portions of the Software.
316
+ Issues and pull requests welcome on [GitHub](https://github.com/cesswhite/solo-analytics).
317
+
318
+ ## License
411
319
 
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.
320
+ [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