clarity-analytics-sdk 1.0.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 +46 -0
- package/package.json +41 -0
- package/src/index.js +557 -0
- package/src/staging.js +565 -0
- package/src/uat.js +567 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Clarity Analytics SDK
|
|
2
|
+
|
|
3
|
+
A lightweight JavaScript SDK for tracking events and analytics with Clarity Analytics.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install clarity-analytics-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const ClarityAnalytics = require('clarity-analytics-sdk');
|
|
15
|
+
|
|
16
|
+
// Initialize the SDK
|
|
17
|
+
const analytics = new ClarityAnalytics({
|
|
18
|
+
baseUrl: 'https://api.yourbackend.com',
|
|
19
|
+
apiUrl: 'https://api.yourbackend.com/api/v1/events/publish-event',
|
|
20
|
+
projectId: 'your-project-id',
|
|
21
|
+
debug: true
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Wait for SDK to be ready
|
|
25
|
+
await analytics.ready();
|
|
26
|
+
|
|
27
|
+
// Track events
|
|
28
|
+
analytics.track('Button Click', {
|
|
29
|
+
button_id: 'signup',
|
|
30
|
+
page: '/landing'
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
| Parameter | Required | Description |
|
|
37
|
+
|-----------|----------|-------------|
|
|
38
|
+
| `baseUrl` | Yes | Base URL for your backend API |
|
|
39
|
+
| `apiUrl` | Yes | Full URL for the events endpoint |
|
|
40
|
+
| `projectId` | Yes | Your project identifier |
|
|
41
|
+
| `debug` | No | Enable debug logging (default: false) |
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT © Vivaconnect
|
|
46
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clarity-analytics-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight JavaScript SDK for tracking events and analytics with Clarity Analytics",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"prepublishOnly": "npm run validate",
|
|
8
|
+
"validate": "node -c src/index.js",
|
|
9
|
+
"mend": "mend code --inc -d . -s clarity_backend//sdk-website --report --formats pdf",
|
|
10
|
+
"standard": "standard"
|
|
11
|
+
},
|
|
12
|
+
"author": "Vivaconnect",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"files": [
|
|
15
|
+
"src/"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=12.0.0"
|
|
19
|
+
},
|
|
20
|
+
"standard": {
|
|
21
|
+
"env": [
|
|
22
|
+
"browser",
|
|
23
|
+
"node"
|
|
24
|
+
],
|
|
25
|
+
"globals": [
|
|
26
|
+
"localStorage",
|
|
27
|
+
"ClarityAnalytics",
|
|
28
|
+
"getProjectId",
|
|
29
|
+
"getSessionId",
|
|
30
|
+
"getUserId",
|
|
31
|
+
"initSDK"
|
|
32
|
+
],
|
|
33
|
+
"ignore": [
|
|
34
|
+
"src/staging.js"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"standard": "^17.1.2",
|
|
39
|
+
"terser": "^5.44.1"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity Analytics SDK
|
|
3
|
+
* A lightweight JavaScript SDK for tracking events and analytics
|
|
4
|
+
* Version: 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
(function (window) {
|
|
8
|
+
'use strict'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Clarity Analytics SDK Class
|
|
12
|
+
*/
|
|
13
|
+
class ClarityAnalytics {
|
|
14
|
+
constructor (config = {}) {
|
|
15
|
+
if (!config.projectId) {
|
|
16
|
+
throw new Error('ClarityAnalytics: projectId is required')
|
|
17
|
+
}
|
|
18
|
+
// Configuration
|
|
19
|
+
this.config = {
|
|
20
|
+
apiUrl: 'https://apps.helo.ai/api/v1/clarity/events/publish-event',
|
|
21
|
+
fetchUserIdUrl: 'https://apps.helo.ai/api/v1/clarity/projects?id=',
|
|
22
|
+
// apiUrl: 'http://localhost:9030/api/v1/events/publish-event',
|
|
23
|
+
// fetchUserIdUrl: 'http://localhost:9030/api/v1/projects?id=',
|
|
24
|
+
projectId: config.projectId,
|
|
25
|
+
projectName: config.projectName || '',
|
|
26
|
+
apiKey: config.apiKey || null,
|
|
27
|
+
userId: null, // Will be fetched from backend
|
|
28
|
+
sessionId: config.sessionId || this._generateSessionId(),
|
|
29
|
+
debug: config.debug || false,
|
|
30
|
+
retryAttempts: config.retryAttempts || 3,
|
|
31
|
+
timeout: config.timeout || 10000,
|
|
32
|
+
batchSize: config.batchSize || 1, // For future batching feature
|
|
33
|
+
flushInterval: config.flushInterval || 5000 // For future batching feature
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Internal state
|
|
37
|
+
this._eventQueue = []
|
|
38
|
+
this._isInitialized = false
|
|
39
|
+
this._initializationPromise = null
|
|
40
|
+
this._deviceInfo = this._getDeviceInfo()
|
|
41
|
+
this._sessionStart = Date.now()
|
|
42
|
+
|
|
43
|
+
// Initialize SDK (fetch user ID from backend)
|
|
44
|
+
this._initializationPromise = this._initialize(config)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize SDK - Fetch user ID from backend
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
async _initialize (config) {
|
|
52
|
+
try {
|
|
53
|
+
// Fetch user ID from backend
|
|
54
|
+
const { userId, ipaddress } = await this._fetchUserId(this.config.projectId)
|
|
55
|
+
|
|
56
|
+
if (!userId) {
|
|
57
|
+
throw new Error('Failed to fetch user ID from backend')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.config.userId = userId
|
|
61
|
+
this.config.ipaddress = ipaddress
|
|
62
|
+
this._isInitialized = true
|
|
63
|
+
|
|
64
|
+
// Auto-collect page view if enabled
|
|
65
|
+
if (config.autoPageView !== false) {
|
|
66
|
+
this._trackPageView()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return this
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this._isInitialized = false
|
|
72
|
+
throw new Error(`ClarityAnalytics initialization failed: ${error.message}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Wait for SDK to be ready
|
|
78
|
+
* @returns {Promise} Resolves when SDK is initialized
|
|
79
|
+
*/
|
|
80
|
+
async ready () {
|
|
81
|
+
return this._initializationPromise
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fetch user ID from backend
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
async _fetchUserId (projectId) {
|
|
89
|
+
const url = `${this.config.fetchUserIdUrl}${projectId}`
|
|
90
|
+
|
|
91
|
+
const controller = new AbortController()
|
|
92
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout)
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(url, {
|
|
96
|
+
method: 'GET',
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` })
|
|
100
|
+
},
|
|
101
|
+
signal: controller.signal
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
clearTimeout(timeoutId)
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const responseData = await response.json()
|
|
111
|
+
// Extract userId from response (supports multiple response structures)
|
|
112
|
+
let userId = null
|
|
113
|
+
let ipaddress = null
|
|
114
|
+
|
|
115
|
+
// Check if response has a 'data' wrapper
|
|
116
|
+
if (responseData?.data) {
|
|
117
|
+
userId = responseData?.data?.userId
|
|
118
|
+
ipaddress = responseData?.data?.ipaddress
|
|
119
|
+
} else {
|
|
120
|
+
userId = responseData?.userId
|
|
121
|
+
ipaddress = responseData?.ipaddress
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!userId) {
|
|
125
|
+
throw new Error('User ID not found in API response')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { userId, ipaddress }
|
|
129
|
+
} catch (error) {
|
|
130
|
+
clearTimeout(timeoutId)
|
|
131
|
+
throw error
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Track a custom event
|
|
137
|
+
* @param {string} eventName - Name of the event
|
|
138
|
+
* @param {Object} properties - Event properties
|
|
139
|
+
* @param {Object} options - Additional options
|
|
140
|
+
*/
|
|
141
|
+
async track (eventName, properties = {}, options = {}) {
|
|
142
|
+
console.log({ eventName, properties, options })
|
|
143
|
+
// Wait for SDK to be initialized
|
|
144
|
+
if (!this._isInitialized) {
|
|
145
|
+
try {
|
|
146
|
+
await this._initializationPromise
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return Promise.reject(new Error('SDK not initialized: ' + error.message))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Double check initialization status
|
|
153
|
+
if (!this._isInitialized) {
|
|
154
|
+
return Promise.reject(new Error('SDK not initialized'))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!eventName) {
|
|
158
|
+
throw new Error('Event name is required')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const eventData = this._buildEventPayload({
|
|
162
|
+
eventType: options.eventType || 'custom',
|
|
163
|
+
eventName,
|
|
164
|
+
eventKey: options.eventKey || this._sanitizeEventName(eventName),
|
|
165
|
+
properties,
|
|
166
|
+
sessionId: properties.sessionId || options.sessionId,
|
|
167
|
+
context: options.context || {},
|
|
168
|
+
traits: options.traits || {},
|
|
169
|
+
ipaddress: options.ipaddress || this.config.ipaddress,
|
|
170
|
+
customId: properties.userId || options.userId
|
|
171
|
+
})
|
|
172
|
+
return this._sendEvent(eventData)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Track a page view
|
|
177
|
+
* @param {Object} properties - Page properties
|
|
178
|
+
* @param {Object} options - Additional options
|
|
179
|
+
*/
|
|
180
|
+
// async page (properties = {}, options = {}) {
|
|
181
|
+
// const pageName = properties.name || (typeof document !== 'undefined' ? document.title : 'Page View')
|
|
182
|
+
|
|
183
|
+
// return this.track('page_view', {
|
|
184
|
+
// ...properties,
|
|
185
|
+
// page_name: pageName
|
|
186
|
+
// }, {
|
|
187
|
+
// ...options,
|
|
188
|
+
// eventType: 'pageview',
|
|
189
|
+
// eventKey: 'page_view'
|
|
190
|
+
// })
|
|
191
|
+
// }
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Build complete event payload
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
_buildEventPayload (data) {
|
|
198
|
+
const timestamp = Date.now()
|
|
199
|
+
const eventId = this._generateUUID()
|
|
200
|
+
const requestId = this._generateRequestId()
|
|
201
|
+
const userId = data.customId || this.config.userId
|
|
202
|
+
|
|
203
|
+
// Determine system_user_id (real user ID) vs anonymous user_id
|
|
204
|
+
let systemUserId = ''
|
|
205
|
+
const finalUserId = userId
|
|
206
|
+
|
|
207
|
+
if (userId && !userId.startsWith('user_')) {
|
|
208
|
+
// If it's a real user ID provided by the application
|
|
209
|
+
systemUserId = userId
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Build flat event payload (matching staging.js structure)
|
|
213
|
+
return {
|
|
214
|
+
// Event identifiers
|
|
215
|
+
event_id: eventId,
|
|
216
|
+
request_id: requestId,
|
|
217
|
+
event_name: data.eventName || 'Unnamed Event',
|
|
218
|
+
timestamp,
|
|
219
|
+
|
|
220
|
+
// User and session
|
|
221
|
+
user_id: finalUserId,
|
|
222
|
+
session_id: data.sessionId || this.config.sessionId,
|
|
223
|
+
system_user_id: systemUserId,
|
|
224
|
+
|
|
225
|
+
// Project information
|
|
226
|
+
project_id: this.config.projectId,
|
|
227
|
+
project_name: this.config.projectName || '',
|
|
228
|
+
|
|
229
|
+
// Device information (flattened)
|
|
230
|
+
device: this._deviceInfo.device.type,
|
|
231
|
+
os: this._deviceInfo.device.os,
|
|
232
|
+
os_version: this._deviceInfo.device.osVersion,
|
|
233
|
+
model: this._deviceInfo.device.model,
|
|
234
|
+
brand: this._deviceInfo.device.brand,
|
|
235
|
+
|
|
236
|
+
// Browser information (flattened)
|
|
237
|
+
browser: this._deviceInfo.browser.name,
|
|
238
|
+
browser_version: this._deviceInfo.browser.version,
|
|
239
|
+
browser_engine: this._deviceInfo.browser.engine,
|
|
240
|
+
|
|
241
|
+
// Location information (flattened)
|
|
242
|
+
timezone: typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : 'UTC',
|
|
243
|
+
language: typeof navigator !== 'undefined' ? navigator.language : 'en-US',
|
|
244
|
+
|
|
245
|
+
// Network information (flattened)
|
|
246
|
+
network_type: this._getConnectionType(),
|
|
247
|
+
|
|
248
|
+
// SDK information (flattened)
|
|
249
|
+
sdk_name: 'clarity',
|
|
250
|
+
sdk_version: '1.0.0',
|
|
251
|
+
|
|
252
|
+
// IP address
|
|
253
|
+
ip: data.ipaddress || this.config.ipaddress || '',
|
|
254
|
+
|
|
255
|
+
// Properties object with all custom data
|
|
256
|
+
properties: {
|
|
257
|
+
page_url: (typeof window !== 'undefined' && window.location) ? window.location.href : '',
|
|
258
|
+
page_title: (typeof document !== 'undefined') ? document.title : '',
|
|
259
|
+
page_path: (typeof window !== 'undefined' && window.location) ? window.location.pathname : '',
|
|
260
|
+
referrer: (typeof document !== 'undefined') ? (document.referrer || 'direct') : 'direct',
|
|
261
|
+
eventType: data.eventType || 'custom',
|
|
262
|
+
eventKey: data.eventKey || this._sanitizeEventName(data.eventName || ''),
|
|
263
|
+
// Custom properties
|
|
264
|
+
...(data.properties || {})
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// Context for any additional data
|
|
268
|
+
context: {
|
|
269
|
+
...(data.context || {})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Send event to API
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
async _sendEvent (eventData, attempt = 1) {
|
|
279
|
+
try {
|
|
280
|
+
const headers = {
|
|
281
|
+
'Content-Type': 'application/json'
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (this.config.apiKey) {
|
|
285
|
+
headers.Authorization = `Bearer ${this.config.apiKey}`
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const controller = new AbortController()
|
|
289
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout)
|
|
290
|
+
console.log('eventData', eventData)
|
|
291
|
+
const response = await fetch(this.config.apiUrl, {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
headers,
|
|
294
|
+
body: JSON.stringify(eventData),
|
|
295
|
+
signal: controller.signal
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
clearTimeout(timeoutId)
|
|
299
|
+
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const result = await response.json()
|
|
305
|
+
return result
|
|
306
|
+
} catch (error) {
|
|
307
|
+
// Retry logic
|
|
308
|
+
if (attempt < this.config.retryAttempts && !error.name === 'AbortError') {
|
|
309
|
+
const delay = Math.pow(2, attempt) * 1000 // Exponential backoff
|
|
310
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
311
|
+
return this._sendEvent(eventData, attempt + 1)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
throw error
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Track initial page view
|
|
320
|
+
* @private
|
|
321
|
+
*/
|
|
322
|
+
_trackPageView () {
|
|
323
|
+
// Skip in Node.js environment
|
|
324
|
+
if (typeof document === 'undefined') {
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Wait for DOM to be ready
|
|
329
|
+
if (document.readyState === 'loading') {
|
|
330
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
331
|
+
setTimeout(() => this.page(), 100)
|
|
332
|
+
})
|
|
333
|
+
} else {
|
|
334
|
+
setTimeout(() => this.page(), 100)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get device and browser information
|
|
340
|
+
* @private
|
|
341
|
+
*/
|
|
342
|
+
_getDeviceInfo () {
|
|
343
|
+
let isNodeJS = false
|
|
344
|
+
let ua = 'Unknown'
|
|
345
|
+
|
|
346
|
+
// Detect runtime environment
|
|
347
|
+
if (typeof window === 'undefined' && typeof process !== 'undefined' && process.versions && process.versions.node) {
|
|
348
|
+
// Node.js
|
|
349
|
+
isNodeJS = true
|
|
350
|
+
ua = 'Node.js'
|
|
351
|
+
} else if (typeof navigator !== 'undefined' && navigator.userAgent) {
|
|
352
|
+
// Browser
|
|
353
|
+
ua = navigator.userAgent
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const device = {
|
|
357
|
+
type: 'unknown',
|
|
358
|
+
brand: 'Unknown',
|
|
359
|
+
model: 'Unknown',
|
|
360
|
+
os: 'Unknown',
|
|
361
|
+
osVersion: 'Unknown'
|
|
362
|
+
}
|
|
363
|
+
const browser = {
|
|
364
|
+
name: 'Unknown',
|
|
365
|
+
version: 'Unknown',
|
|
366
|
+
engine: 'Unknown'
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (isNodeJS) {
|
|
370
|
+
// Node.js specifics
|
|
371
|
+
device.type = 'server'
|
|
372
|
+
device.os = process.platform || 'Node.js'
|
|
373
|
+
device.osVersion = process.version || 'Unknown'
|
|
374
|
+
|
|
375
|
+
browser.name = 'Node.js'
|
|
376
|
+
browser.version = process.version || 'Unknown'
|
|
377
|
+
browser.engine = 'V8'
|
|
378
|
+
} else if (ua !== 'Unknown') {
|
|
379
|
+
// Browser specifics
|
|
380
|
+
|
|
381
|
+
// Device type
|
|
382
|
+
if (/Mobile|Android|iPhone|iPad|iPod/i.test(ua)) {
|
|
383
|
+
device.type = 'mobile'
|
|
384
|
+
} else if (/Tablet|iPad/i.test(ua)) {
|
|
385
|
+
device.type = 'tablet'
|
|
386
|
+
} else {
|
|
387
|
+
device.type = 'desktop'
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// OS Detection
|
|
391
|
+
if (/Windows NT 10.0/.test(ua)) {
|
|
392
|
+
device.os = 'Windows'
|
|
393
|
+
device.osVersion = '10'
|
|
394
|
+
} else if (/Windows NT 6.3/.test(ua)) {
|
|
395
|
+
device.os = 'Windows'
|
|
396
|
+
device.osVersion = '8.1'
|
|
397
|
+
} else if (/Windows NT 6.2/.test(ua)) {
|
|
398
|
+
device.os = 'Windows'
|
|
399
|
+
device.osVersion = '8'
|
|
400
|
+
} else if (/Windows NT 6.1/.test(ua)) {
|
|
401
|
+
device.os = 'Windows'
|
|
402
|
+
device.osVersion = '7'
|
|
403
|
+
} else if (/Mac OS X ([0-9_]+)/.test(ua)) {
|
|
404
|
+
device.os = 'macOS'
|
|
405
|
+
const match = ua.match(/Mac OS X ([0-9_]+)/)
|
|
406
|
+
if (match) {
|
|
407
|
+
device.osVersion = match[1].replace(/_/g, '.')
|
|
408
|
+
}
|
|
409
|
+
} else if (/Android ([0-9.]+)/.test(ua)) {
|
|
410
|
+
device.os = 'Android'
|
|
411
|
+
const match = ua.match(/Android ([0-9.]+)/)
|
|
412
|
+
if (match) {
|
|
413
|
+
device.osVersion = match[1]
|
|
414
|
+
}
|
|
415
|
+
} else if (/iPhone|iPad|iPod/.test(ua)) {
|
|
416
|
+
device.os = 'iOS'
|
|
417
|
+
const match = ua.match(/OS ([0-9_]+)/)
|
|
418
|
+
if (match) {
|
|
419
|
+
device.osVersion = match[1].replace(/_/g, '.')
|
|
420
|
+
}
|
|
421
|
+
} else if (/Linux/.test(ua)) {
|
|
422
|
+
device.os = 'Linux'
|
|
423
|
+
// Try to extract kernel version
|
|
424
|
+
const match = ua.match(/Linux ([0-9.]+)/)
|
|
425
|
+
if (match) {
|
|
426
|
+
device.osVersion = match[1]
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Browser detection (order matters)
|
|
431
|
+
if (ua.includes('Edg/')) {
|
|
432
|
+
browser.name = 'Edge'
|
|
433
|
+
browser.engine = 'Blink'
|
|
434
|
+
const match = ua.match(/Edg\/([0-9.]+)/)
|
|
435
|
+
if (match) browser.version = match[1]
|
|
436
|
+
} else if (ua.includes('OPR/')) {
|
|
437
|
+
browser.name = 'Opera'
|
|
438
|
+
browser.engine = 'Blink'
|
|
439
|
+
const match = ua.match(/OPR\/([0-9.]+)/)
|
|
440
|
+
if (match) browser.version = match[1]
|
|
441
|
+
} else if (ua.includes('Chrome') && !ua.includes('Edg')) {
|
|
442
|
+
// Chrome (should be after Edge/Opera)
|
|
443
|
+
browser.name = 'Chrome'
|
|
444
|
+
browser.engine = 'Blink'
|
|
445
|
+
const match = ua.match(/Chrome\/([0-9.]+)/)
|
|
446
|
+
if (match) browser.version = match[1]
|
|
447
|
+
} else if (ua.includes('Firefox')) {
|
|
448
|
+
browser.name = 'Firefox'
|
|
449
|
+
browser.engine = 'Gecko'
|
|
450
|
+
const match = ua.match(/Firefox\/([0-9.]+)/)
|
|
451
|
+
if (match) browser.version = match[1]
|
|
452
|
+
} else if (ua.includes('Safari') && !ua.includes('Chrome') && !ua.includes('Chromium')) {
|
|
453
|
+
browser.name = 'Safari'
|
|
454
|
+
browser.engine = 'WebKit'
|
|
455
|
+
const match = ua.match(/Version\/([0-9.]+)/)
|
|
456
|
+
if (match) browser.version = match[1]
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return { device, browser }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get network connection type
|
|
464
|
+
* @private
|
|
465
|
+
*/
|
|
466
|
+
_getConnectionType () {
|
|
467
|
+
if (typeof navigator !== 'undefined' && navigator.connection) {
|
|
468
|
+
return navigator.connection.effectiveType || 'unknown'
|
|
469
|
+
}
|
|
470
|
+
return 'unknown'
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Get screen information
|
|
475
|
+
* @private
|
|
476
|
+
*/
|
|
477
|
+
_getScreenInfo () {
|
|
478
|
+
if (typeof window !== 'undefined' && window.screen) {
|
|
479
|
+
return {
|
|
480
|
+
width: window.screen.width || 0,
|
|
481
|
+
height: window.screen.height || 0,
|
|
482
|
+
density: window.devicePixelRatio || 1
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Default for Node.js or environments without screen
|
|
486
|
+
return {
|
|
487
|
+
width: 0,
|
|
488
|
+
height: 0,
|
|
489
|
+
density: 1
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Generate session ID
|
|
495
|
+
* @private
|
|
496
|
+
*/
|
|
497
|
+
_generateSessionId () {
|
|
498
|
+
return 'sess_' + this._generateId()
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Generate UUID v4
|
|
503
|
+
* @private
|
|
504
|
+
*/
|
|
505
|
+
_generateUUID () {
|
|
506
|
+
// Simple UUID v4 generator for browsers without crypto.randomUUID
|
|
507
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
508
|
+
return crypto.randomUUID()
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Fallback UUID v4 generator
|
|
512
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
513
|
+
const r = Math.random() * 16 | 0
|
|
514
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
|
515
|
+
return v.toString(16)
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Generate request ID
|
|
521
|
+
* @private
|
|
522
|
+
*/
|
|
523
|
+
_generateRequestId () {
|
|
524
|
+
return 'req_' + this._generateId()
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Generate random ID
|
|
529
|
+
* @private
|
|
530
|
+
*/
|
|
531
|
+
_generateId () {
|
|
532
|
+
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Sanitize event name for use as event key
|
|
537
|
+
* @private
|
|
538
|
+
*/
|
|
539
|
+
_sanitizeEventName (name) {
|
|
540
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, '_')
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Export for different module systems
|
|
545
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
546
|
+
// Node.js
|
|
547
|
+
module.exports = ClarityAnalytics
|
|
548
|
+
} else if (typeof define === 'function' && define.amd) { // eslint-disable-line no-undef
|
|
549
|
+
// AMD
|
|
550
|
+
define([], function () { // eslint-disable-line no-undef
|
|
551
|
+
return ClarityAnalytics
|
|
552
|
+
})
|
|
553
|
+
} else {
|
|
554
|
+
// Browser globals
|
|
555
|
+
window.ClarityAnalytics = ClarityAnalytics
|
|
556
|
+
}
|
|
557
|
+
})(typeof window !== 'undefined' ? window : this)
|