clarity-analytics-sdk 1.0.0 → 1.0.3

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