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/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)