cdp-lite-sdk 0.1.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/package.json +19 -0
- package/src/cdp-lite-sdk.js +393 -0
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cdp-lite-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight JS SDK to push events and manage attributes (demo)",
|
|
5
|
+
"main": "src/cdp-lite-sdk.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": "Vi Nguyen",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
15
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
16
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
17
|
+
"rollup": "^4.54.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// cdp-lite-sdk.test.js
|
|
2
|
+
// VConnect Analytics SDK - Track events and users
|
|
3
|
+
|
|
4
|
+
class CdpLiteSdk {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = {
|
|
7
|
+
apiKey: config.apiKey,
|
|
8
|
+
source: config.source || 'Web',
|
|
9
|
+
serviceName: config.serviceName || 'DefaultService',
|
|
10
|
+
baseUrl: config.baseUrl || 'https://stg-ingestlog.vietcredit.com.vn',
|
|
11
|
+
isTest: config.isTest !== undefined ? config.isTest : false,
|
|
12
|
+
debug: config.debug || false,
|
|
13
|
+
batchSize: config.batchSize || 10,
|
|
14
|
+
batchInterval: config.batchInterval || 5000, // 5 seconds
|
|
15
|
+
autoTrackDevice: config.autoTrackDevice !== undefined ? config.autoTrackDevice : true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
this.eventQueue = [];
|
|
19
|
+
this.userId = null;
|
|
20
|
+
this.anonymousId = this._getOrCreateAnonymousId();
|
|
21
|
+
this.userTraits = {};
|
|
22
|
+
this.deviceInfo = this.config.autoTrackDevice ? this._getDeviceInfo() : {};
|
|
23
|
+
|
|
24
|
+
// Start batch processor
|
|
25
|
+
if (this.config.batchSize > 1) {
|
|
26
|
+
this._startBatchProcessor();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this._log('VConnect Analytics initialized', this.config);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============ Public Methods ============
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Track an event
|
|
36
|
+
* @param {string} eventName - Name of the event
|
|
37
|
+
* @param {object} properties - Event properties
|
|
38
|
+
* @param {object} options - Additional options (device, campaign, context, etc.)
|
|
39
|
+
*/
|
|
40
|
+
track(eventName, properties = {}, options = {}) {
|
|
41
|
+
const event = this._createEvent({
|
|
42
|
+
type: 'track',
|
|
43
|
+
event: eventName,
|
|
44
|
+
properties,
|
|
45
|
+
...options
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (this.config.batchSize > 1) {
|
|
49
|
+
this._addToQueue(event);
|
|
50
|
+
} else {
|
|
51
|
+
return this._sendEvent(event);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Identify a user
|
|
57
|
+
* @param {string} userId - User ID
|
|
58
|
+
* @param {object} traits - User traits/attributes
|
|
59
|
+
*/
|
|
60
|
+
identify(userId, traits = {}) {
|
|
61
|
+
this.userId = userId;
|
|
62
|
+
this.userTraits = { ...this.userTraits, ...traits };
|
|
63
|
+
|
|
64
|
+
const event = this._createEvent({
|
|
65
|
+
type: 'identify',
|
|
66
|
+
event: 'user_identified',
|
|
67
|
+
traits,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this._log('User identified', { userId, traits });
|
|
71
|
+
|
|
72
|
+
if (this.config.batchSize > 1) {
|
|
73
|
+
this._addToQueue(event);
|
|
74
|
+
} else {
|
|
75
|
+
return this._sendEvent(event);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Track a page view
|
|
81
|
+
* @param {string} pageName - Page name
|
|
82
|
+
* @param {object} properties - Page properties
|
|
83
|
+
*/
|
|
84
|
+
page(pageName, properties = {}) {
|
|
85
|
+
return this.track('page_view', {
|
|
86
|
+
page_name: pageName,
|
|
87
|
+
...properties
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Track a screen view (for mobile apps)
|
|
93
|
+
* @param {string} screenName - Screen name
|
|
94
|
+
* @param {object} properties - Screen properties
|
|
95
|
+
*/
|
|
96
|
+
screen(screenName, properties = {}) {
|
|
97
|
+
return this.track('screen_view', {
|
|
98
|
+
screen_name: screenName,
|
|
99
|
+
...properties
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Set user properties
|
|
105
|
+
* @param {object} traits - User traits
|
|
106
|
+
*/
|
|
107
|
+
setUserAttributes(traits) {
|
|
108
|
+
this.userTraits = { ...this.userTraits, ...traits };
|
|
109
|
+
return this.identify(this.userId, traits);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set device information
|
|
114
|
+
* @param {object} deviceInfo - Device information
|
|
115
|
+
*/
|
|
116
|
+
setDeviceInfo(deviceInfo) {
|
|
117
|
+
this.deviceInfo = { ...this.deviceInfo, ...deviceInfo };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Manually flush the event queue
|
|
122
|
+
*/
|
|
123
|
+
async flush() {
|
|
124
|
+
if (this.eventQueue.length === 0) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const events = [...this.eventQueue];
|
|
129
|
+
this.eventQueue = [];
|
|
130
|
+
|
|
131
|
+
return this._sendBatch(events);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reset user data (logout)
|
|
136
|
+
*/
|
|
137
|
+
reset() {
|
|
138
|
+
this.userId = null;
|
|
139
|
+
this.userTraits = {};
|
|
140
|
+
this.anonymousId = this._generateUUID();
|
|
141
|
+
this._saveAnonymousId(this.anonymousId);
|
|
142
|
+
this._log('User data reset');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============ Private Methods ============
|
|
146
|
+
|
|
147
|
+
_createEvent(data) {
|
|
148
|
+
const event = {
|
|
149
|
+
event_id: this._generateUUID(),
|
|
150
|
+
type: data.type || 'track',
|
|
151
|
+
event: data.event,
|
|
152
|
+
service_name: this.config.serviceName,
|
|
153
|
+
user_id: this.userId || '',
|
|
154
|
+
anonymous_id: this.anonymousId,
|
|
155
|
+
loan_code: data.loanCode || '',
|
|
156
|
+
properties: data.properties || {},
|
|
157
|
+
traits: data.traits || this.userTraits,
|
|
158
|
+
device: data.device || this.deviceInfo,
|
|
159
|
+
campaign: data.campaign || {},
|
|
160
|
+
context: data.context || {},
|
|
161
|
+
event_time: new Date().toISOString(),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return event;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async _sendEvent(event) {
|
|
168
|
+
const url = `${this.config.baseUrl}/api/v1/events/track`;
|
|
169
|
+
const headers = this._getHeaders();
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const response = await this._makeRequest(url, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers,
|
|
175
|
+
body: JSON.stringify(event),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this._log('Event tracked successfully', event);
|
|
179
|
+
return response;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this._logError('Failed to track event', error);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async _sendBatch(events) {
|
|
187
|
+
if (events.length === 0) return;
|
|
188
|
+
|
|
189
|
+
const url = `${this.config.baseUrl}/api/v1/events/batch`;
|
|
190
|
+
const headers = this._getHeaders();
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const response = await this._makeRequest(url, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers,
|
|
196
|
+
body: JSON.stringify({ events }),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this._log(`Batch of ${events.length} events tracked successfully`);
|
|
200
|
+
return response;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this._logError('Failed to track batch', error);
|
|
203
|
+
// Re-queue failed events
|
|
204
|
+
this.eventQueue.unshift(...events);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_addToQueue(event) {
|
|
210
|
+
this.eventQueue.push(event);
|
|
211
|
+
this._log('Event added to queue', { queueSize: this.eventQueue.length });
|
|
212
|
+
|
|
213
|
+
if (this.eventQueue.length >= this.config.batchSize) {
|
|
214
|
+
this.flush();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_startBatchProcessor() {
|
|
219
|
+
this.batchInterval = setInterval(() => {
|
|
220
|
+
if (this.eventQueue.length > 0) {
|
|
221
|
+
this.flush();
|
|
222
|
+
}
|
|
223
|
+
}, this.config.batchInterval);
|
|
224
|
+
|
|
225
|
+
// Clean up on page unload (browser only)
|
|
226
|
+
if (typeof window !== 'undefined') {
|
|
227
|
+
window.addEventListener('beforeunload', () => {
|
|
228
|
+
this.flush();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_getHeaders() {
|
|
234
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
'X-Api-Key': this.config.apiKey,
|
|
238
|
+
'X-Source': this.config.source,
|
|
239
|
+
'X-Timestamp': timestamp.toString(),
|
|
240
|
+
'X-Signatures': '', // Implement signature logic if needed
|
|
241
|
+
'isTest': this.config.isTest.toString(),
|
|
242
|
+
'Content-Type': 'application/json',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async _makeRequest(url, options) {
|
|
247
|
+
// Support both browser fetch and Node.js
|
|
248
|
+
if (typeof fetch !== 'undefined') {
|
|
249
|
+
const response = await fetch(url, options);
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
252
|
+
}
|
|
253
|
+
return response.json();
|
|
254
|
+
} else {
|
|
255
|
+
// For Node.js environment
|
|
256
|
+
const https = require('https');
|
|
257
|
+
const urlObj = new URL(url);
|
|
258
|
+
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
const req = https.request({
|
|
261
|
+
hostname: urlObj.hostname,
|
|
262
|
+
path: urlObj.pathname + urlObj.search,
|
|
263
|
+
method: options.method,
|
|
264
|
+
headers: options.headers,
|
|
265
|
+
}, (res) => {
|
|
266
|
+
let data = '';
|
|
267
|
+
res.on('data', chunk => data += chunk);
|
|
268
|
+
res.on('end', () => {
|
|
269
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
270
|
+
resolve(JSON.parse(data));
|
|
271
|
+
} else {
|
|
272
|
+
reject(new Error(`HTTP error! status: ${res.statusCode}`));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
req.on('error', reject);
|
|
278
|
+
if (options.body) {
|
|
279
|
+
req.write(options.body);
|
|
280
|
+
}
|
|
281
|
+
req.end();
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_getDeviceInfo() {
|
|
287
|
+
if (typeof window === 'undefined') {
|
|
288
|
+
// Node.js environment
|
|
289
|
+
return {
|
|
290
|
+
platform: process.platform,
|
|
291
|
+
brand: 'Server',
|
|
292
|
+
model: 'Node.js',
|
|
293
|
+
app_version: '',
|
|
294
|
+
os_version: process.version,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Browser environment
|
|
299
|
+
const ua = navigator.userAgent;
|
|
300
|
+
const platform = this._detectPlatform(ua);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
platform,
|
|
304
|
+
brand: this._detectBrand(ua),
|
|
305
|
+
model: this._detectModel(ua),
|
|
306
|
+
app_version: '',
|
|
307
|
+
os_version: this._detectOSVersion(ua),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
_detectPlatform(ua) {
|
|
312
|
+
if (/iPhone|iPad|iPod/.test(ua)) return 'ios';
|
|
313
|
+
if (/Android/.test(ua)) return 'android';
|
|
314
|
+
if (/Windows/.test(ua)) return 'windows';
|
|
315
|
+
if (/Mac/.test(ua)) return 'macos';
|
|
316
|
+
if (/Linux/.test(ua)) return 'linux';
|
|
317
|
+
return 'web';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_detectBrand(ua) {
|
|
321
|
+
if (/iPhone|iPad|iPod/.test(ua)) return 'Apple';
|
|
322
|
+
if (/Samsung/.test(ua)) return 'Samsung';
|
|
323
|
+
if (/Huawei/.test(ua)) return 'Huawei';
|
|
324
|
+
if (/Xiaomi/.test(ua)) return 'Xiaomi';
|
|
325
|
+
if (/Oppo/.test(ua)) return 'Oppo';
|
|
326
|
+
return 'Unknown';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
_detectModel(ua) {
|
|
330
|
+
const match = ua.match(/\(([^)]+)\)/);
|
|
331
|
+
return match ? match[1].split(';')[0].trim() : 'Unknown';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_detectOSVersion(ua) {
|
|
335
|
+
const match = ua.match(/(?:Android|iPhone OS|CPU OS|Mac OS X|Windows NT) ([\d._]+)/);
|
|
336
|
+
return match ? match[1].replace(/_/g, '.') : 'Unknown';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
_getOrCreateAnonymousId() {
|
|
340
|
+
if (typeof localStorage !== 'undefined') {
|
|
341
|
+
let id = localStorage.getItem('vconnect_anonymous_id');
|
|
342
|
+
if (!id) {
|
|
343
|
+
id = this._generateUUID();
|
|
344
|
+
this._saveAnonymousId(id);
|
|
345
|
+
}
|
|
346
|
+
return id;
|
|
347
|
+
}
|
|
348
|
+
return this._generateUUID();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
_saveAnonymousId(id) {
|
|
352
|
+
if (typeof localStorage !== 'undefined') {
|
|
353
|
+
localStorage.setItem('vconnect_anonymous_id', id);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_generateUUID() {
|
|
358
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
359
|
+
const r = Math.random() * 16 | 0;
|
|
360
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
361
|
+
return v.toString(16);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
_log(...args) {
|
|
366
|
+
if (this.config.debug) {
|
|
367
|
+
console.log('[VConnect Analytics]', ...args);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_logError(...args) {
|
|
372
|
+
if (this.config.debug) {
|
|
373
|
+
console.error('[VConnect Analytics]', ...args);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Cleanup
|
|
378
|
+
destroy() {
|
|
379
|
+
if (this.batchInterval) {
|
|
380
|
+
clearInterval(this.batchInterval);
|
|
381
|
+
}
|
|
382
|
+
this.flush();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Export for different module systems
|
|
387
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
388
|
+
module.exports = CdpLiteSdk;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (typeof window !== 'undefined') {
|
|
392
|
+
window.CdpLiteSdk = CdpLiteSdk;
|
|
393
|
+
}
|