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 +2 -3
- package/src/index.js +36 -28
- package/README.md +0 -46
- package/src/staging.js +0 -565
- package/src/uat.js +0 -567
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clarity-analytics-sdk",
|
|
3
|
-
"version": "1.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: '
|
|
21
|
-
fetchUserIdUrl: '
|
|
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
|
-
|
|
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
|
-
//
|
|
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)
|