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