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.
Files changed (2) hide show
  1. package/package.json +19 -0
  2. 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
+ }