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 +228 -326
- 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,320 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Solo Analytics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Read browser context once. Use it anywhere.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
37
|
-
console.log(
|
|
38
|
-
console.log(
|
|
39
|
-
console.log(
|
|
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
|
-
//
|
|
42
|
-
|
|
28
|
+
// Clean up when you're done (SPAs, component unmount)
|
|
29
|
+
analytics.destroy();
|
|
43
30
|
```
|
|
44
31
|
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
import { useSoloAnalytics } from "solo-analytics";
|
|
34
|
+
---
|
|
49
35
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
Despite the `use` prefix (Vue convention), the function is **framework-agnostic**. Same API in React, Nuxt, Svelte, or plain JavaScript.
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
---
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
- `destroy()`: Cleans up all event listeners and timers to prevent memory leaks
|
|
75
|
+
## The data object
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
`analytics.data` is a `SoloAnalyticsInfo` object. Here is what each section contains and when you might use it.
|
|
74
78
|
|
|
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
|
|
79
|
+
### Browser, OS, and device
|
|
82
80
|
|
|
83
|
-
|
|
81
|
+
Parsed from `navigator.userAgent`:
|
|
84
82
|
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
### Screen
|
|
90
100
|
|
|
91
101
|
```typescript
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
107
|
+
Useful for responsive decisions, screenshot tooling, or debugging layout issues reported by users.
|
|
174
108
|
|
|
175
|
-
###
|
|
109
|
+
### Network
|
|
176
110
|
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
|
|
111
|
+
```typescript
|
|
112
|
+
analytics.data.network;
|
|
113
|
+
// online, effectiveType ("4g", "3g", "slow-2g"…)
|
|
114
|
+
// downlink (Mbps), rtt (ms), saveData
|
|
115
|
+
```
|
|
180
116
|
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
###
|
|
127
|
+
### Environment and privacy signals
|
|
217
128
|
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
230
|
-
import { onMounted, onUnmounted, ref, computed } from "vue";
|
|
231
|
-
import { useSoloAnalytics, SoloAnalyticsReturn } from "solo-analytics";
|
|
136
|
+
### Optional async fields
|
|
232
137
|
|
|
233
|
-
|
|
138
|
+
Populated after idle-time probes (or immediately after `await analytics.refresh()`):
|
|
234
139
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
if (!analytics.value) return "Unknown";
|
|
178
|
+
Only sync data — no idle callbacks, no async probes:
|
|
247
179
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
180
|
+
```typescript
|
|
181
|
+
const analytics = useSoloAnalytics({
|
|
182
|
+
detectFeatures: false,
|
|
183
|
+
trackVisibility: false,
|
|
251
184
|
});
|
|
252
|
-
</script>
|
|
253
185
|
```
|
|
254
186
|
|
|
255
|
-
|
|
187
|
+
---
|
|
256
188
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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 {
|
|
275
|
-
import { useSoloAnalytics
|
|
197
|
+
import { onMounted, onUnmounted, ref } from "vue";
|
|
198
|
+
import { useSoloAnalytics } from "solo-analytics";
|
|
276
199
|
|
|
277
|
-
const
|
|
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
|
|
282
|
-
|
|
283
|
-
autoRefresh: true,
|
|
284
|
-
refreshInterval: 15000,
|
|
285
|
-
});
|
|
204
|
+
analytics = useSoloAnalytics();
|
|
205
|
+
isMobile.value = analytics.isMobile;
|
|
286
206
|
});
|
|
287
207
|
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
const analytics = useSoloAnalytics();
|
|
216
|
+
### React
|
|
312
217
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
222
|
+
function App() {
|
|
223
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
333
224
|
|
|
334
|
-
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
const analytics = useSoloAnalytics();
|
|
227
|
+
setIsMobile(analytics.isMobile);
|
|
228
|
+
return () => analytics.destroy();
|
|
229
|
+
}, []);
|
|
335
230
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
235
|
+
### Vanilla JS
|
|
345
236
|
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
# Install dependencies
|
|
350
|
-
npm install
|
|
239
|
+
---
|
|
351
240
|
|
|
352
|
-
|
|
353
|
-
npm run dev
|
|
241
|
+
## API reference
|
|
354
242
|
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
359
|
-
npm test
|
|
256
|
+
### Exported types
|
|
360
257
|
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
273
|
+
### Reactivity note
|
|
366
274
|
|
|
367
|
-
|
|
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
|
-
|
|
370
|
-
|--------|-------|
|
|
371
|
-
| ESM bundle (gzip) | ~3.5 KB |
|
|
372
|
-
| UMD bundle (gzip) | ~3.1 KB |
|
|
373
|
-
| `sideEffects` | `false` (tree-shakeable) |
|
|
277
|
+
---
|
|
374
278
|
|
|
375
|
-
|
|
279
|
+
## Limitations (read before relying on edge cases)
|
|
376
280
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
-
|
|
380
|
-
|
|
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
|
-
|
|
290
|
+
---
|
|
383
291
|
|
|
384
|
-
|
|
292
|
+
## Bundle size
|
|
385
293
|
|
|
386
|
-
|
|
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
|
-
|
|
296
|
+
---
|
|
393
297
|
|
|
394
|
-
|
|
298
|
+
## Development
|
|
395
299
|
|
|
396
|
-
|
|
300
|
+
This project uses [Vite+](https://viteplus.dev/) (`vp` CLI: Vite, Vitest, Oxlint, Oxfmt, tsdown).
|
|
397
301
|
|
|
398
|
-
|
|
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
|
-
|
|
312
|
+
---
|
|
401
313
|
|
|
402
|
-
|
|
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
|
-
|
|
410
|
-
|
|
316
|
+
Issues and pull requests welcome on [GitHub](https://github.com/cesswhite/solo-analytics).
|
|
317
|
+
|
|
318
|
+
## License
|
|
411
319
|
|
|
412
|
-
|
|
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
|