flux-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/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "flux-analytics-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Flux Analytics SDK for JavaScript/Web",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [
10
+ "flux",
11
+ "analytics",
12
+ "sdk",
13
+ "web"
14
+ ],
15
+ "author": "Flux Team",
16
+ "license": "ISC",
17
+ "dependencies": {
18
+ "uuid": "^9.0.1"
19
+ }
20
+ }
package/src/Flux.js ADDED
@@ -0,0 +1,467 @@
1
+
2
+ const Flux = (function () {
3
+ // Private state
4
+ let _appId = null;
5
+ let _firmId = null;
6
+ let _userId = null;
7
+ let _debug = false;
8
+ let _isProcessingQueue = false;
9
+
10
+ // Session Management
11
+ let _sessionId = null;
12
+ let _sessionStartTime = null;
13
+ let _initialized = false;
14
+
15
+ // Base URL for API
16
+ let _baseUrl = 'https://cnpawrfdekifurtkdblw.supabase.co/rest/v1';
17
+ let _apiKey = null;
18
+
19
+ // Constants
20
+ const _queueKey = 'flux_event_queue';
21
+ const _installKey = 'flux_app_installed';
22
+ const _propsKey = 'flux_user_properties';
23
+ const _anonIdKey = 'flux_anonymous_id';
24
+ const _sessionKey = 'flux_session_id'; // To persist session ID if needed
25
+
26
+ let _campaignId = null;
27
+ let _userProperties = {};
28
+ let _anonymousId = null;
29
+ const _screenTimers = {};
30
+
31
+ // MMP / Attribution Data
32
+ let _attributionData = {};
33
+ let _clickId = null;
34
+
35
+ // Helper: Generate UUID (Simple v4 implementation for browser if library not available)
36
+ function uuidv4() {
37
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
38
+ return crypto.randomUUID();
39
+ }
40
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
41
+ const r = (Math.random() * 16) | 0,
42
+ v = c === 'x' ? r : (r & 0x3) | 0x8;
43
+ return v.toString(16);
44
+ });
45
+ }
46
+
47
+ // Helper: Get Device Info
48
+ function getDeviceInfo() {
49
+ const ua = navigator.userAgent;
50
+ const platform = navigator.platform || 'unknown';
51
+ const language = navigator.language || 'en-US';
52
+ const screenRes = `${window.screen.width}x${window.screen.height}`;
53
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
54
+
55
+ return {
56
+ platform: 'web',
57
+ os_version: platform, // Web specific
58
+ device_model: ua, // User Agent as model
59
+ language: language,
60
+ timezone: timeZone,
61
+ resolution: screenRes,
62
+ user_agent: ua
63
+ };
64
+ }
65
+
66
+ // Helper: LocalStorage Wrapper
67
+ const Storage = {
68
+ get: (key) => localStorage.getItem(key),
69
+ set: (key, value) => localStorage.setItem(key, value),
70
+ remove: (key) => localStorage.removeItem(key),
71
+ getJSON: (key) => {
72
+ try {
73
+ return JSON.parse(localStorage.getItem(key));
74
+ } catch (e) {
75
+ return null;
76
+ }
77
+ },
78
+ setJSON: (key, value) => localStorage.setItem(key, JSON.stringify(value))
79
+ };
80
+
81
+
82
+ // ----------------------------------------------------------------------
83
+ // Private Methods
84
+ // ----------------------------------------------------------------------
85
+
86
+ async function _initAnonymousId() {
87
+ try {
88
+ // 1. Check Storage
89
+ _anonymousId = Storage.get(_anonIdKey);
90
+
91
+ if (!_anonymousId) {
92
+ // 2. Generate new if not exists
93
+ _anonymousId = uuidv4();
94
+ Storage.set(_anonIdKey, _anonymousId);
95
+ if (_debug) console.log('Flux: New Anonymous ID generated:', _anonymousId);
96
+ } else {
97
+ if (_debug) console.log('Flux: Existing Anonymous ID loaded:', _anonymousId);
98
+ }
99
+ } catch (e) {
100
+ if (_debug) console.error('Flux: Error initializing Anonymous ID:', e);
101
+ _anonymousId = 'unknown_device';
102
+ }
103
+ }
104
+
105
+ async function _loadUserProperties() {
106
+ try {
107
+ const props = Storage.getJSON(_propsKey);
108
+ if (props) {
109
+ _userProperties = props;
110
+ }
111
+ } catch (e) {
112
+ if (_debug) console.error('Flux Load Props Error:', e);
113
+ }
114
+ }
115
+
116
+ async function _checkAttribution() {
117
+ // Web attribution logic can be implemented here
118
+ // Checking URL parameters for campaign_id, click_id, gclid etc.
119
+ const urlParams = new URLSearchParams(window.location.search);
120
+
121
+ // Example: ?campaign_id=...&click_id=...
122
+ const campId = urlParams.get('campaign_id') || urlParams.get('utm_campaign');
123
+ const clickId = urlParams.get('click_id') || urlParams.get('gclid');
124
+
125
+ if (campId) _campaignId = campId;
126
+ if (clickId) _clickId = clickId;
127
+
128
+ // Save if found
129
+ if (campId || clickId) {
130
+ // Logic to persist attribution if needed
131
+ if (_debug) console.log(`Flux: Attribution found. Campaign: ${campId}, Click: ${clickId}`);
132
+ }
133
+ }
134
+
135
+ async function _startSession() {
136
+ if (_sessionId) return; // Session already active
137
+
138
+ // Check if we can recover a session from storage (optional, for page reloads)
139
+ // For now, let's treat every page load/init as a new session start for simplicity in Web Context
140
+ // OR implement session timeout logic (e.g. 30 mins inactivity)
141
+
142
+ _sessionId = uuidv4();
143
+ _sessionStartTime = new Date();
144
+
145
+ if (_debug) console.log('Flux: Session Started (ID: ' + _sessionId + ')');
146
+
147
+ // Sessions tablosuna oturum başlattığımızı bildir
148
+ await _trackSessionData({ isStart: true });
149
+
150
+ // Otomatik "session_start" olayı kaydı
151
+ await track('session_start');
152
+ }
153
+
154
+ async function _trackSessionData({ isStart }) {
155
+ const deviceInfo = getDeviceInfo();
156
+
157
+ const sessionData = {
158
+ session_id: _sessionId,
159
+ device_id: _anonymousId,
160
+ user_id: _userId || _anonymousId,
161
+ app_id: _appId,
162
+ firm_id: _firmId,
163
+ session_start: _sessionStartTime.toISOString(),
164
+ platform: deviceInfo.platform,
165
+ is_crashed: false // Hard to detect crashes on web in same way
166
+ };
167
+
168
+ if (!isStart) {
169
+ const now = new Date();
170
+ sessionData.session_end = now.toISOString();
171
+ if (_sessionStartTime) {
172
+ sessionData.duration = Math.floor((now - _sessionStartTime) / 1000);
173
+ }
174
+ }
175
+
176
+ await _addToQueue('sessions', sessionData);
177
+ }
178
+
179
+ // Handle Page Visibility API for Session End/Resume?
180
+ // For web, "session end" is tricky (tab close). existing 'beforeunload' event is unreliable for async calls.
181
+ // relying on 'sendBeacon' or simple keep-alive is better.
182
+ // For now, we will just track session_start.
183
+
184
+ async function _checkInstall() {
185
+ try {
186
+ const isInstalled = Storage.get(_installKey);
187
+
188
+ if (!isInstalled) {
189
+ if (_debug) console.log('Flux: First visit (Install) detected...');
190
+
191
+ // 1. Installs tablosuna kaydet
192
+ await _trackInstall();
193
+
194
+ await track('app_install'); // Log as event too
195
+ await track('first_open');
196
+
197
+ Storage.set(_installKey, 'true');
198
+ if (_debug) console.log('Flux: First visit marked.');
199
+ }
200
+ } catch (e) {
201
+ if (_debug) console.error('Flux Install Check Error:', e);
202
+ }
203
+ }
204
+
205
+ async function _trackInstall() {
206
+ const deviceInfo = getDeviceInfo();
207
+ const installData = {
208
+ id: uuidv4(),
209
+ firm_id: _firmId,
210
+ app_id: _appId,
211
+ user_id: _userId || _anonymousId,
212
+ device_id: _anonymousId,
213
+ anonymous_id: _anonymousId,
214
+ platform: deviceInfo.platform,
215
+ os_version: deviceInfo.os_version,
216
+ device_model: deviceInfo.device_model,
217
+ connection_type: navigator.connection ? navigator.connection.effectiveType : 'unknown',
218
+ install_date: new Date().toISOString(),
219
+ source: 'web_direct',
220
+ attribution_status: _campaignId ? 'attributed' : 'unattributed',
221
+ campaign_id: _campaignId
222
+ };
223
+ await _addToQueue('installs', installData);
224
+ }
225
+
226
+ async function _addToQueue(endpoint, data) {
227
+ try {
228
+ let currentQueue = Storage.getJSON(_queueKey) || [];
229
+ currentQueue.push({ endpoint, data });
230
+ Storage.setJSON(_queueKey, currentQueue);
231
+
232
+ // Trigger process
233
+ _processQueue();
234
+ } catch (e) {
235
+ if (_debug) console.error('Flux Error adding to queue:', e);
236
+ }
237
+ }
238
+
239
+ async function _processQueue() {
240
+ if (_isProcessingQueue) return;
241
+
242
+ // Check online status
243
+ if (!navigator.onLine) return;
244
+
245
+ _isProcessingQueue = true;
246
+
247
+ try {
248
+ // Processing loop
249
+ while (true) {
250
+ let currentQueue = Storage.getJSON(_queueKey) || [];
251
+ if (currentQueue.length === 0) break;
252
+
253
+ const item = currentQueue[0];
254
+ if (_debug) console.log(`Flux Processing Queue (${currentQueue.length} remaining)...`);
255
+
256
+ const success = await _sendToBackend(item.endpoint, item.data);
257
+
258
+ if (success) {
259
+ // Remove from queue
260
+ currentQueue = Storage.getJSON(_queueKey) || []; // Re-read to be safe
261
+ if (currentQueue.length > 0) { // simple check, race condition possible but low risk single-thread JS
262
+ currentQueue.shift();
263
+ Storage.setJSON(_queueKey, currentQueue);
264
+ }
265
+ } else {
266
+ if (_debug) console.log('Flux: Queue processing paused usage due to failure.');
267
+ break;
268
+ }
269
+ }
270
+ } catch (e) {
271
+ if (_debug) console.error('Flux Queue Error:', e);
272
+ } finally {
273
+ _isProcessingQueue = false;
274
+ }
275
+ }
276
+
277
+ async function _sendToBackend(endpoint, data) {
278
+ if (!_apiKey) {
279
+ if (_debug) console.warn('Flux Warning: API Key is missing.');
280
+ return false;
281
+ }
282
+
283
+ try {
284
+ let url;
285
+ let headers = {
286
+ 'Content-Type': 'application/json',
287
+ 'apikey': _apiKey,
288
+ 'Authorization': `Bearer ${_apiKey}`
289
+ };
290
+ let body = JSON.stringify(data);
291
+
292
+ // Special handling for 'events' -> RPC
293
+ if (endpoint === 'events') {
294
+ url = `${_baseUrl}/rpc/insert_event_v2`;
295
+ body = JSON.stringify({ event_data: data });
296
+ } else if (endpoint === 'sessions') {
297
+ url = `${_baseUrl}/sessions?on_conflict=session_id`;
298
+ headers['Prefer'] = 'return=representation,resolution=merge-duplicates';
299
+ } else if (endpoint === 'installs') {
300
+ url = `${_baseUrl}/installs`;
301
+ headers['Prefer'] = 'return=representation';
302
+ } else {
303
+ url = `${_baseUrl}/${endpoint}`;
304
+ }
305
+
306
+ if (_debug) console.log(`Flux: Posting to ${url}`, body);
307
+
308
+ const response = await fetch(url, {
309
+ method: 'POST',
310
+ headers: headers,
311
+ body: body
312
+ });
313
+
314
+ // 2xx success, 409 conflict (duplicate) is also success
315
+ if (response.ok || response.status === 409) {
316
+ return true;
317
+ } else {
318
+ if (_debug) console.error(`Flux Error: ${response.status} - ${response.statusText}`);
319
+ const text = await response.text();
320
+ if (_debug) console.error('Response:', text);
321
+ return false;
322
+ }
323
+
324
+ } catch (e) {
325
+ if (_debug) console.error('Flux Network Error:', e);
326
+ return false;
327
+ }
328
+ }
329
+
330
+ // Auto tracking
331
+ function _setupAutoTrack() {
332
+ // Track visibility change to manage session
333
+ document.addEventListener("visibilitychange", () => {
334
+ if (document.visibilityState === 'visible') {
335
+ // Resume or start new session?
336
+ // For now, keep simple.
337
+ } else {
338
+ // Hidden
339
+ }
340
+ });
341
+
342
+ // Online/Offline listeners
343
+ window.addEventListener('online', _processQueue);
344
+ }
345
+
346
+
347
+ // ----------------------------------------------------------------------
348
+ // Public API
349
+ // ----------------------------------------------------------------------
350
+
351
+ /**
352
+ * Initialize Flux SDK
353
+ * @param {string} appId - The Application ID (UUID)
354
+ * @param {string} firmId - The Firm ID (UUID)
355
+ * @param {object} config - Optional config { apiKey, baseUrl, debug }
356
+ */
357
+ async function init(appId, firmId, config = {}) {
358
+ _appId = appId;
359
+ _firmId = firmId;
360
+ _apiKey = config.apiKey || null;
361
+ if (config.baseUrl) _baseUrl = config.baseUrl;
362
+ _debug = config.debug || false;
363
+
364
+ if (_debug) console.log('Flux SDK Initializing...');
365
+
366
+ await _initAnonymousId();
367
+ await _checkAttribution();
368
+ await _loadUserProperties();
369
+ await _startSession();
370
+ await _checkInstall();
371
+
372
+ _setupAutoTrack();
373
+ _processQueue(); // Process any old events
374
+
375
+ _initialized = true;
376
+ }
377
+
378
+ /**
379
+ * Identify a user
380
+ * @param {string} userId
381
+ */
382
+ function identify(userId) {
383
+ _userId = userId;
384
+ if (_debug) console.log('Flux Identified User:', _userId);
385
+ // Maybe trigger an identify event?
386
+ }
387
+
388
+ /**
389
+ * Track an event
390
+ * @param {string} eventName
391
+ * @param {object} metadata
392
+ */
393
+ async function track(eventName, metadata = {}) {
394
+ if (!_appId || !_firmId) {
395
+ console.error('Flux: SDK not initialized.');
396
+ return;
397
+ }
398
+
399
+ const deviceInfo = getDeviceInfo();
400
+
401
+ const payload = {
402
+ id: uuidv4(),
403
+ firm_id: _firmId,
404
+ app_id: _appId,
405
+ user_id: _userId || _anonymousId, // fallback to anon if no user
406
+ anonymous_id: _anonymousId,
407
+ campaign_id: _campaignId,
408
+ session_id: _sessionId,
409
+ device_id: _anonymousId,
410
+ game_type: eventName, // reusing this field for event name as per schema
411
+ latency_ms: 0,
412
+ is_error: false,
413
+ error_details: null,
414
+ metadata: {
415
+ ..._userProperties,
416
+ ...metadata,
417
+ ..._attributionData,
418
+ platform: deviceInfo.platform,
419
+ os_version: deviceInfo.os_version,
420
+ device_model: deviceInfo.device_model, // User Agent
421
+ // app_version: ???
422
+ language: deviceInfo.language,
423
+ timezone: deviceInfo.timezone,
424
+ resolution: deviceInfo.resolution,
425
+ page_url: window.location.href,
426
+ page_title: document.title,
427
+ referrer: document.referrer
428
+ },
429
+ created_at: new Date().toISOString()
430
+ };
431
+
432
+ if (_debug) console.log(`Flux Tracked: ${eventName}`, payload);
433
+
434
+ await _addToQueue('events', payload);
435
+ }
436
+
437
+ /**
438
+ * Track a page view
439
+ * @param {string} name - Optional page name
440
+ * @param {object} metadata
441
+ */
442
+ async function trackScreen(name, metadata = {}) {
443
+ const screenName = name || document.title || window.location.pathname;
444
+ await track('screen_view', {
445
+ screen_name: screenName,
446
+ ...metadata
447
+ });
448
+ }
449
+
450
+
451
+ return {
452
+ init,
453
+ identify,
454
+ track,
455
+ trackScreen,
456
+ // Expose for debugging if needed
457
+ _debug: () => _debug
458
+ };
459
+
460
+ })();
461
+
462
+ // Export for module systems
463
+ if (typeof module !== 'undefined' && module.exports) {
464
+ module.exports = Flux;
465
+ } else if (typeof window !== 'undefined') {
466
+ window.Flux = Flux;
467
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+
2
+ const Flux = require('./Flux');
3
+
4
+ module.exports = Flux;
package/test.html ADDED
@@ -0,0 +1,38 @@
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Flux JS SDK Test</title>
8
+ </head>
9
+ <body>
10
+ <h1>Flux JS SDK Test</h1>
11
+ <button id="trackBtn">Track Event</button>
12
+ <button id="identifyBtn">Identify User</button>
13
+ <div id="status">Converting...</div>
14
+
15
+ <script src="src/Flux.js"></script>
16
+ <script>
17
+ const APP_ID = '93a6e925-04ee-4e5a-9ae0-bc59592f87c3'; // Example App ID
18
+ const FIRM_ID = '93a6e925-04ee-4e5a-9ae0-bc59592f87c3'; // Example Firm ID
19
+ const API_KEY = 'YOUR_SUPABASE_ANON_KEY'; // Need to replace this with real key for full test
20
+
21
+ console.log('Initializing Flux...');
22
+ Flux.init(APP_ID, FIRM_ID, {
23
+ debug: true,
24
+ // apiKey: API_KEY // Uncomment to test with real backend
25
+ }).then(() => {
26
+ document.getElementById('status').innerText = 'Flux Initialized!';
27
+ });
28
+
29
+ document.getElementById('trackBtn').addEventListener('click', () => {
30
+ Flux.track('button_click', { button_id: 'trackBtn' });
31
+ });
32
+
33
+ document.getElementById('identifyBtn').addEventListener('click', () => {
34
+ Flux.identify('user_' + Math.floor(Math.random() * 1000));
35
+ });
36
+ </script>
37
+ </body>
38
+ </html>