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