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 +73 -371
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +125 -0
- package/dist/index.d.ts +124 -3
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -36
- package/dist/composables/useSoloAnalytics.d.ts +0 -46
- package/dist/composables/useSoloAnalytics.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/solo-analytics.js +0 -475
- package/dist/solo-analytics.js.map +0 -1
- package/dist/solo-analytics.umd.cjs +0 -2
- package/dist/solo-analytics.umd.cjs.map +0 -1
- package/dist/types/analytics.d.ts +0 -87
- package/dist/types/analytics.d.ts.map +0 -1
- package/dist/utils/features.d.ts +0 -24
- package/dist/utils/features.d.ts.map +0 -1
- package/dist/utils/location.d.ts +0 -6
- package/dist/utils/location.d.ts.map +0 -1
- package/dist/utils/network.d.ts +0 -6
- package/dist/utils/network.d.ts.map +0 -1
- package/dist/utils/parseUserAgent.d.ts +0 -16
- package/dist/utils/parseUserAgent.d.ts.map +0 -1
- package/dist/utils/performance.d.ts +0 -6
- package/dist/utils/performance.d.ts.map +0 -1
- package/dist/utils/scheduleIdle.d.ts +0 -6
- package/dist/utils/scheduleIdle.d.ts.map +0 -1
- package/dist/utils/screen.d.ts +0 -6
- package/dist/utils/screen.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,418 +1,120 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Solo Analytics
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/solo-analytics)
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
-
[](https://github.com/cesswhite/solo-analytics/blob/main/LICENSE)
|
|
8
|
-
[](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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
Use it when you need to:
|
|
69
14
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
88
|
-
|
|
89
|
-
The `data` object contains the following structure:
|
|
34
|
+
## Usage
|
|
90
35
|
|
|
91
36
|
```typescript
|
|
92
|
-
|
|
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
|
-
|
|
242
|
-
analytics.value?.destroy();
|
|
243
|
-
});
|
|
39
|
+
const analytics = useSoloAnalytics();
|
|
244
40
|
|
|
245
|
-
|
|
246
|
-
|
|
41
|
+
console.log(analytics.isMobile);
|
|
42
|
+
console.log(analytics.browserName);
|
|
43
|
+
console.log(analytics.data);
|
|
247
44
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return "Desktop";
|
|
251
|
-
});
|
|
252
|
-
</script>
|
|
45
|
+
// When done (SPA unmount, etc.)
|
|
46
|
+
analytics.destroy();
|
|
253
47
|
```
|
|
254
48
|
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
63
|
+
Set `detectFeatures: false` if you only need UA, screen, and network data. That skips the async work entirely.
|
|
305
64
|
|
|
306
|
-
|
|
307
|
-
<script type="module">
|
|
308
|
-
import { useSoloAnalytics } from "solo-analytics";
|
|
65
|
+
### API
|
|
309
66
|
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
321
|
-
"browserInfo"
|
|
322
|
-
).textContent = `${analytics.browserName} on ${analytics.osName}`;
|
|
79
|
+
### SSR
|
|
323
80
|
|
|
324
|
-
|
|
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
|
-
|
|
83
|
+
### Limitations
|
|
333
84
|
|
|
334
|
-
|
|
85
|
+
Be aware of what this library can and cannot guarantee:
|
|
335
86
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
-
|
|
339
|
-
|
|
340
|
-
-
|
|
341
|
-
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
|
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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"}
|