web-manager 4.0.35 → 4.0.37
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/CLAUDE.md +0 -0
- package/README.md +1 -1
- package/dist/index.js +52 -10
- package/dist/modules/analytics.js +250 -0
- package/dist/modules/usage.js +264 -0
- package/dist/modules/utilities.js +174 -187
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
Binary file
|
package/README.md
CHANGED
|
@@ -249,7 +249,7 @@ Manager.utilities(); // Utility functions
|
|
|
249
249
|
Manager.isDevelopment(); // Check if in development mode
|
|
250
250
|
Manager.getFunctionsUrl(); // Get Firebase Functions URL
|
|
251
251
|
Manager.getFunctionsUrl('development'); // Force development URL
|
|
252
|
-
Manager.getApiUrl(); // Get API URL (derived from
|
|
252
|
+
Manager.getApiUrl(); // Get API URL (derived from brand url)
|
|
253
253
|
Manager.isValidRedirectUrl('https://...'); // Validate redirect URL
|
|
254
254
|
|
|
255
255
|
// Firebase instances (after initialization)
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import Storage from './modules/storage.js';
|
|
2
|
-
import
|
|
2
|
+
import Utilities from './modules/utilities.js';
|
|
3
3
|
import * as domUtils from './modules/dom.js';
|
|
4
|
+
import Analytics from './modules/analytics.js';
|
|
4
5
|
import Auth from './modules/auth.js';
|
|
5
6
|
import Bindings from './modules/bindings.js';
|
|
6
7
|
import Firestore from './modules/firestore.js';
|
|
7
8
|
import Notifications from './modules/notifications.js';
|
|
8
9
|
import ServiceWorker from './modules/service-worker.js';
|
|
9
10
|
import Sentry from './modules/sentry.js';
|
|
11
|
+
import Usage from './modules/usage.js';
|
|
10
12
|
|
|
11
13
|
class Manager {
|
|
12
14
|
constructor() {
|
|
@@ -23,12 +25,15 @@ class Manager {
|
|
|
23
25
|
|
|
24
26
|
// Initialize modules
|
|
25
27
|
this._storage = new Storage();
|
|
28
|
+
this._utilities = new Utilities(this);
|
|
29
|
+
this._analytics = new Analytics(this);
|
|
26
30
|
this._auth = new Auth(this);
|
|
27
31
|
this._bindings = new Bindings(this);
|
|
28
32
|
this._firestore = new Firestore(this);
|
|
29
33
|
this._notifications = new Notifications(this);
|
|
30
34
|
this._serviceWorker = new ServiceWorker(this);
|
|
31
35
|
this._sentry = new Sentry(this);
|
|
36
|
+
this._usage = new Usage(this);
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
// Module getters
|
|
@@ -60,13 +65,21 @@ class Manager {
|
|
|
60
65
|
return this._sentry;
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
usage() {
|
|
69
|
+
return this._usage;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
analytics() {
|
|
73
|
+
return this._analytics;
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
// DOM utilities
|
|
64
77
|
dom() {
|
|
65
78
|
return domUtils;
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
utilities() {
|
|
69
|
-
return
|
|
82
|
+
return this._utilities;
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
// Initialize the manager
|
|
@@ -88,6 +101,16 @@ class Manager {
|
|
|
88
101
|
await this._sentry.init(this.config.sentry.config);
|
|
89
102
|
}
|
|
90
103
|
|
|
104
|
+
// Initialize Analytics if tracking ID and secret are provided
|
|
105
|
+
if (this.config.tracking?.['google-analytics'] && this.config.tracking?.['google-analytics-secret']) {
|
|
106
|
+
this._analytics.init({
|
|
107
|
+
id: this.config.tracking['google-analytics'],
|
|
108
|
+
secret: this.config.tracking['google-analytics-secret'],
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
console.log('[Analytics] Skipped: missing google-analytics ID or secret');
|
|
112
|
+
}
|
|
113
|
+
|
|
91
114
|
// Initialize service worker if enabled
|
|
92
115
|
if (this.config.serviceWorker?.enabled) {
|
|
93
116
|
this._serviceWorker.register({
|
|
@@ -111,8 +134,14 @@ class Manager {
|
|
|
111
134
|
// Old IE force polyfill
|
|
112
135
|
// await this._loadPolyfillsIfNeeded();
|
|
113
136
|
|
|
114
|
-
//
|
|
115
|
-
this.
|
|
137
|
+
// Initialize usage tracking
|
|
138
|
+
await this._usage.initialize();
|
|
139
|
+
|
|
140
|
+
// Update bindings with config and usage data
|
|
141
|
+
this.bindings().update({
|
|
142
|
+
config: this.config,
|
|
143
|
+
usage: this._usage.getBindingData(),
|
|
144
|
+
});
|
|
116
145
|
|
|
117
146
|
return this;
|
|
118
147
|
} catch (error) {
|
|
@@ -124,6 +153,7 @@ class Manager {
|
|
|
124
153
|
_processConfiguration(configuration) {
|
|
125
154
|
// Default configuration structure
|
|
126
155
|
const defaults = {
|
|
156
|
+
runtime: null, // Auto-detect if not provided (web, browser-extension, electron, node)
|
|
127
157
|
environment: 'production',
|
|
128
158
|
buildTime: Date.now(),
|
|
129
159
|
brand: {
|
|
@@ -266,6 +296,12 @@ class Manager {
|
|
|
266
296
|
path: '/service-worker.js'
|
|
267
297
|
}
|
|
268
298
|
},
|
|
299
|
+
tracking: {
|
|
300
|
+
'google-analytics': '',
|
|
301
|
+
'google-analytics-secret': '',
|
|
302
|
+
'meta-pixel': '',
|
|
303
|
+
'tiktok-pixel': '',
|
|
304
|
+
},
|
|
269
305
|
};
|
|
270
306
|
|
|
271
307
|
// Deep merge configuration with defaults
|
|
@@ -284,6 +320,11 @@ class Manager {
|
|
|
284
320
|
merged.refreshNewVersion.config.interval = safeEvaluate(merged.refreshNewVersion.config.interval);
|
|
285
321
|
}
|
|
286
322
|
|
|
323
|
+
// Calculate buildTimeISO from buildTime
|
|
324
|
+
if (merged.buildTime) {
|
|
325
|
+
merged.buildTimeISO = new Date(merged.buildTime).toISOString();
|
|
326
|
+
}
|
|
327
|
+
|
|
287
328
|
// Return merged configuration
|
|
288
329
|
return merged;
|
|
289
330
|
}
|
|
@@ -318,13 +359,13 @@ class Manager {
|
|
|
318
359
|
const $html = document.documentElement;
|
|
319
360
|
|
|
320
361
|
// Set platform (OS) - windows, mac, linux, ios, android, chromeos, unknown
|
|
321
|
-
$html.dataset.platform =
|
|
362
|
+
$html.dataset.platform = this._utilities.getPlatform();
|
|
322
363
|
|
|
323
|
-
// Set runtime - web, extension, electron, node
|
|
324
|
-
$html.dataset.runtime =
|
|
364
|
+
// Set runtime - web, browser-extension, electron, node
|
|
365
|
+
$html.dataset.runtime = this._utilities.getRuntime();
|
|
325
366
|
|
|
326
367
|
// Set device type - mobile, tablet, desktop
|
|
327
|
-
$html.dataset.device =
|
|
368
|
+
$html.dataset.device = this._utilities.getDeviceType();
|
|
328
369
|
}
|
|
329
370
|
|
|
330
371
|
async _initializeFirebase() {
|
|
@@ -417,8 +458,9 @@ class Manager {
|
|
|
417
458
|
return 'http://localhost:5002';
|
|
418
459
|
}
|
|
419
460
|
|
|
420
|
-
const authDomain = this.config.firebase.app.config.authDomain;
|
|
421
|
-
const
|
|
461
|
+
// const authDomain = this.config.firebase.app.config.authDomain;
|
|
462
|
+
const apiDomain = this.config.brand.url;
|
|
463
|
+
const baseUrl = url || (apiDomain ? `https://${apiDomain}` : window.location.origin);
|
|
422
464
|
const urlObj = new URL(baseUrl);
|
|
423
465
|
const hostnameParts = urlObj.hostname.split('.');
|
|
424
466
|
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Dev mode credentials by runtime
|
|
2
|
+
const DEV_CREDENTIALS = {
|
|
3
|
+
'browser-extension': {
|
|
4
|
+
id: 'G-5NWE9SPEEM',
|
|
5
|
+
secret: '33E5W1cCQGKPK4lyMMOWOQ',
|
|
6
|
+
},
|
|
7
|
+
'electron': {
|
|
8
|
+
id: 'G-WMNERKK9J2',
|
|
9
|
+
secret: 'UeKzq8UvS3GD5D2aHcuZgQ',
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Supported runtimes for analytics
|
|
14
|
+
const SUPPORTED_RUNTIMES = ['browser-extension', 'electron'];
|
|
15
|
+
|
|
16
|
+
class Analytics {
|
|
17
|
+
constructor(manager) {
|
|
18
|
+
this.manager = manager;
|
|
19
|
+
this.initialized = false;
|
|
20
|
+
this.devMode = false;
|
|
21
|
+
this.runtime = null;
|
|
22
|
+
this.config = null;
|
|
23
|
+
this.measurementId = null;
|
|
24
|
+
this.secret = null;
|
|
25
|
+
this.clientId = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if runtime is supported
|
|
29
|
+
_isSupported() {
|
|
30
|
+
return SUPPORTED_RUNTIMES.includes(this.runtime);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Initialize analytics
|
|
34
|
+
init(config = {}) {
|
|
35
|
+
// Store config
|
|
36
|
+
this.config = config;
|
|
37
|
+
|
|
38
|
+
// Get runtime
|
|
39
|
+
this.runtime = this.manager.utilities().getRuntime();
|
|
40
|
+
|
|
41
|
+
// TODO: Add web runtime support
|
|
42
|
+
if (!this._isSupported()) {
|
|
43
|
+
console.log(`[Analytics] Runtime "${this.runtime}" not supported yet, skipping`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Skip if already initialized
|
|
48
|
+
if (this.initialized) {
|
|
49
|
+
console.log('[Analytics] Already initialized');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check for development mode
|
|
54
|
+
this.devMode = this.manager.isDevelopment();
|
|
55
|
+
|
|
56
|
+
// Get measurement ID and secret (use dev credentials in dev mode)
|
|
57
|
+
if (this.devMode) {
|
|
58
|
+
const devCreds = DEV_CREDENTIALS[this.runtime];
|
|
59
|
+
if (devCreds) {
|
|
60
|
+
this.measurementId = devCreds.id;
|
|
61
|
+
this.secret = devCreds.secret;
|
|
62
|
+
console.log(`[Analytics] Dev mode: using ${this.runtime} dev credentials`);
|
|
63
|
+
} else {
|
|
64
|
+
// No dev credentials for this runtime, use provided config
|
|
65
|
+
this.measurementId = config.measurementId || config.id;
|
|
66
|
+
this.secret = config.secret;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
this.measurementId = config.measurementId || config.id;
|
|
70
|
+
this.secret = config.secret;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Skip if no measurement ID
|
|
74
|
+
if (!this.measurementId) {
|
|
75
|
+
console.log('[Analytics] No measurement ID provided, skipping initialization');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Generate or retrieve client ID
|
|
80
|
+
this.clientId = this._getClientId();
|
|
81
|
+
|
|
82
|
+
// Log initialization
|
|
83
|
+
console.log(`[Analytics] Initializing with measurement ID: ${this.measurementId}${this.devMode ? ' (dev mode)' : ''} [${this.runtime}]`);
|
|
84
|
+
|
|
85
|
+
// Mark as initialized
|
|
86
|
+
this.initialized = true;
|
|
87
|
+
|
|
88
|
+
// Send initial pageview
|
|
89
|
+
this.event('page_view');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get or generate client ID
|
|
93
|
+
_getClientId() {
|
|
94
|
+
const storageKey = '_ga_client_id';
|
|
95
|
+
|
|
96
|
+
// Try to get existing client ID
|
|
97
|
+
let clientId = null;
|
|
98
|
+
try {
|
|
99
|
+
clientId = localStorage.getItem(storageKey);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// localStorage not available
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Generate new client ID if needed
|
|
105
|
+
if (!clientId) {
|
|
106
|
+
clientId = `${Math.random().toString(36).substring(2)}.${Date.now()}`;
|
|
107
|
+
try {
|
|
108
|
+
localStorage.setItem(storageKey, clientId);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// localStorage not available
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return clientId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get page data to include with all events
|
|
118
|
+
_getPageData() {
|
|
119
|
+
return {
|
|
120
|
+
page_path: window.location.pathname,
|
|
121
|
+
page_title: document.title,
|
|
122
|
+
page_location: window.location.href,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Track an event
|
|
127
|
+
event(eventName, params = {}) {
|
|
128
|
+
// TODO: Add web runtime support
|
|
129
|
+
if (!this._isSupported()) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!this.initialized) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Merge page data with provided params
|
|
138
|
+
const eventParams = {
|
|
139
|
+
...this._getPageData(),
|
|
140
|
+
...params,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Log event
|
|
144
|
+
console.log(`[Analytics] Event: ${eventName}${this.devMode ? ' (dev mode)' : ''}`, eventParams);
|
|
145
|
+
|
|
146
|
+
// Send via Measurement Protocol (fetch)
|
|
147
|
+
this._sendViaFetch(eventName, eventParams);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Send event via Measurement Protocol (fetch)
|
|
151
|
+
_sendViaFetch(eventName, params = {}) {
|
|
152
|
+
// Measurement Protocol requires api_secret
|
|
153
|
+
if (!this.secret) {
|
|
154
|
+
console.warn('[Analytics] No API secret provided, cannot send via Measurement Protocol');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const url = `https://www.google-analytics.com/mp/collect?measurement_id=${this.measurementId}&api_secret=${this.secret}`;
|
|
159
|
+
|
|
160
|
+
const payload = {
|
|
161
|
+
client_id: this.clientId,
|
|
162
|
+
events: [{
|
|
163
|
+
name: eventName,
|
|
164
|
+
params: {
|
|
165
|
+
...params,
|
|
166
|
+
engagement_time_msec: 100,
|
|
167
|
+
session_id: this._getSessionId(),
|
|
168
|
+
},
|
|
169
|
+
}],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Send via fetch (fire and forget)
|
|
173
|
+
fetch(url, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
body: JSON.stringify(payload),
|
|
176
|
+
}).catch((err) => {
|
|
177
|
+
console.warn('[Analytics] Failed to send event:', err);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Get or generate session ID
|
|
182
|
+
_getSessionId() {
|
|
183
|
+
const storageKey = '_ga_session_id';
|
|
184
|
+
const sessionTimeout = 30 * 60 * 1000; // 30 minutes
|
|
185
|
+
|
|
186
|
+
let sessionData = null;
|
|
187
|
+
try {
|
|
188
|
+
sessionData = JSON.parse(sessionStorage.getItem(storageKey) || 'null');
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// sessionStorage not available
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
|
|
195
|
+
// Check if session is still valid
|
|
196
|
+
if (sessionData && (now - sessionData.lastActive) < sessionTimeout) {
|
|
197
|
+
sessionData.lastActive = now;
|
|
198
|
+
try {
|
|
199
|
+
sessionStorage.setItem(storageKey, JSON.stringify(sessionData));
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// sessionStorage not available
|
|
202
|
+
}
|
|
203
|
+
return sessionData.id;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Create new session
|
|
207
|
+
const newSession = {
|
|
208
|
+
id: `${Date.now()}`,
|
|
209
|
+
lastActive: now,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
sessionStorage.setItem(storageKey, JSON.stringify(newSession));
|
|
214
|
+
} catch (e) {
|
|
215
|
+
// sessionStorage not available
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return newSession.id;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Set user properties
|
|
222
|
+
setUserProperties(properties = {}) {
|
|
223
|
+
// TODO: Add web runtime support
|
|
224
|
+
if (!this._isSupported()) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!this.initialized) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// TODO: Implement for Measurement Protocol
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Set user ID
|
|
236
|
+
setUserId(userId) {
|
|
237
|
+
// TODO: Add web runtime support
|
|
238
|
+
if (!this._isSupported()) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!this.initialized) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// TODO: Implement for Measurement Protocol
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export default Analytics;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Unit multipliers
|
|
2
|
+
const UNITS = {
|
|
3
|
+
milliseconds: 1,
|
|
4
|
+
seconds: 1000,
|
|
5
|
+
minutes: 1000 * 60,
|
|
6
|
+
hours: 1000 * 60 * 60,
|
|
7
|
+
days: 1000 * 60 * 60 * 24,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Storage key
|
|
11
|
+
const STORAGE_KEY = 'wm_usage';
|
|
12
|
+
|
|
13
|
+
// Session timeout (30 minutes of inactivity = new session)
|
|
14
|
+
const SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
class Usage {
|
|
17
|
+
constructor(manager) {
|
|
18
|
+
this.manager = manager;
|
|
19
|
+
this.data = null;
|
|
20
|
+
this.initialized = false;
|
|
21
|
+
this.isNewVersion = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if we're in a browser extension context
|
|
25
|
+
_isExtension() {
|
|
26
|
+
return this.manager.utilities().getRuntime() === 'browser-extension';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get extension storage API
|
|
30
|
+
_getExtensionStorage() {
|
|
31
|
+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
|
|
32
|
+
return chrome.storage.local;
|
|
33
|
+
}
|
|
34
|
+
if (typeof browser !== 'undefined' && browser.storage?.local) {
|
|
35
|
+
return browser.storage.local;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Initialize - loads or creates usage data (async for extensions)
|
|
41
|
+
async initialize() {
|
|
42
|
+
// Skip if already initialized
|
|
43
|
+
if (this.initialized) {
|
|
44
|
+
return this.data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Load existing data based on runtime
|
|
48
|
+
let existing = null;
|
|
49
|
+
|
|
50
|
+
if (this._isExtension()) {
|
|
51
|
+
existing = await this._loadFromExtensionStorage();
|
|
52
|
+
} else {
|
|
53
|
+
existing = this._loadFromLocalStorage();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const currentVersion = this.manager.config?.version || null;
|
|
58
|
+
|
|
59
|
+
if (existing) {
|
|
60
|
+
this.data = existing;
|
|
61
|
+
|
|
62
|
+
// Check if this is a new session (last activity was more than SESSION_TIMEOUT ago)
|
|
63
|
+
const timeSinceLastActive = now - (this.data.lastActive || 0);
|
|
64
|
+
if (timeSinceLastActive > SESSION_TIMEOUT) {
|
|
65
|
+
this.data.session.count = (this.data.session?.count || 0) + 1;
|
|
66
|
+
this.data.session.started = now;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Update lastActive
|
|
70
|
+
this.data.lastActive = now;
|
|
71
|
+
|
|
72
|
+
// Check for version change
|
|
73
|
+
if (currentVersion && this.data.version?.current !== currentVersion) {
|
|
74
|
+
this.data.version = this.data.version || {};
|
|
75
|
+
this.data.version.previous = this.data.version.current;
|
|
76
|
+
this.data.version.current = currentVersion;
|
|
77
|
+
this.isNewVersion = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await this._save();
|
|
81
|
+
} else {
|
|
82
|
+
// First time usage
|
|
83
|
+
this.data = {
|
|
84
|
+
installed: now,
|
|
85
|
+
lastActive: now,
|
|
86
|
+
session: {
|
|
87
|
+
started: now,
|
|
88
|
+
count: 1,
|
|
89
|
+
},
|
|
90
|
+
version: {
|
|
91
|
+
initial: currentVersion,
|
|
92
|
+
current: currentVersion,
|
|
93
|
+
previous: null,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
await this._save();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.initialized = true;
|
|
100
|
+
return this.data;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Load from extension storage (async)
|
|
104
|
+
async _loadFromExtensionStorage() {
|
|
105
|
+
const storage = this._getExtensionStorage();
|
|
106
|
+
if (!storage) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const result = await storage.get(STORAGE_KEY);
|
|
112
|
+
return result[STORAGE_KEY] || null;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.warn('[Usage] Failed to load from extension storage:', e);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Load from localStorage (sync)
|
|
120
|
+
_loadFromLocalStorage() {
|
|
121
|
+
try {
|
|
122
|
+
const data = localStorage.getItem(STORAGE_KEY);
|
|
123
|
+
return data ? JSON.parse(data) : null;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Save data to storage
|
|
130
|
+
async _save() {
|
|
131
|
+
if (this._isExtension()) {
|
|
132
|
+
await this._saveToExtensionStorage();
|
|
133
|
+
} else {
|
|
134
|
+
this._saveToLocalStorage();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return this.data;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Save to extension storage (async)
|
|
141
|
+
async _saveToExtensionStorage() {
|
|
142
|
+
const storage = this._getExtensionStorage();
|
|
143
|
+
if (!storage) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await storage.set({ [STORAGE_KEY]: this.data });
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.warn('[Usage] Failed to save to extension storage:', e);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Save to localStorage (sync)
|
|
155
|
+
_saveToLocalStorage() {
|
|
156
|
+
try {
|
|
157
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.data));
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// localStorage not available
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Calculate duration from a timestamp in specified units
|
|
164
|
+
_calculateDuration(timestamp, unit) {
|
|
165
|
+
if (!timestamp) {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Default to milliseconds
|
|
170
|
+
unit = unit || 'milliseconds';
|
|
171
|
+
|
|
172
|
+
// Get multiplier
|
|
173
|
+
const multiplier = UNITS[unit];
|
|
174
|
+
if (!multiplier) {
|
|
175
|
+
throw new Error(`Invalid unit: ${unit}. Valid units: ${Object.keys(UNITS).join(', ')}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return (Date.now() - timestamp) / multiplier;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Get total usage duration in specified units (since installed)
|
|
182
|
+
getUsageDuration(unit) {
|
|
183
|
+
return this._calculateDuration(this.data?.installed, unit);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Get current session duration in specified units
|
|
187
|
+
getSessionDuration(unit) {
|
|
188
|
+
return this._calculateDuration(this.data?.session?.started, unit);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Get installed date
|
|
192
|
+
getInstalledDate() {
|
|
193
|
+
if (!this.data?.installed) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return new Date(this.data.installed);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Get session count
|
|
201
|
+
getSessionCount() {
|
|
202
|
+
return this.data?.session?.count || 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Reset usage data (for testing or user request)
|
|
206
|
+
async reset() {
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
const currentVersion = this.manager.config?.version || null;
|
|
209
|
+
|
|
210
|
+
this.data = {
|
|
211
|
+
installed: now,
|
|
212
|
+
lastActive: now,
|
|
213
|
+
session: {
|
|
214
|
+
started: now,
|
|
215
|
+
count: 1,
|
|
216
|
+
},
|
|
217
|
+
version: {
|
|
218
|
+
initial: currentVersion,
|
|
219
|
+
current: currentVersion,
|
|
220
|
+
previous: null,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
this.isNewVersion = false;
|
|
225
|
+
await this._save();
|
|
226
|
+
return this.data;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Get binding-friendly data object for bindings system
|
|
230
|
+
getBindingData() {
|
|
231
|
+
return {
|
|
232
|
+
installed: this.data?.installed || null,
|
|
233
|
+
lastActive: this.data?.lastActive || null,
|
|
234
|
+
session: {
|
|
235
|
+
started: this.data?.session?.started || null,
|
|
236
|
+
count: this.getSessionCount(),
|
|
237
|
+
},
|
|
238
|
+
version: {
|
|
239
|
+
initial: this.data?.version?.initial || null,
|
|
240
|
+
current: this.data?.version?.current || null,
|
|
241
|
+
previous: this.data?.version?.previous || null,
|
|
242
|
+
isNew: this.isNewVersion,
|
|
243
|
+
},
|
|
244
|
+
duration: {
|
|
245
|
+
total: {
|
|
246
|
+
milliseconds: this.getUsageDuration('milliseconds'),
|
|
247
|
+
seconds: this.getUsageDuration('seconds'),
|
|
248
|
+
minutes: this.getUsageDuration('minutes'),
|
|
249
|
+
hours: this.getUsageDuration('hours'),
|
|
250
|
+
days: this.getUsageDuration('days'),
|
|
251
|
+
},
|
|
252
|
+
session: {
|
|
253
|
+
milliseconds: this.getSessionDuration('milliseconds'),
|
|
254
|
+
seconds: this.getSessionDuration('seconds'),
|
|
255
|
+
minutes: this.getSessionDuration('minutes'),
|
|
256
|
+
hours: this.getSessionDuration('hours'),
|
|
257
|
+
days: this.getSessionDuration('days'),
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default Usage;
|
|
@@ -1,222 +1,209 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
class Utilities {
|
|
2
|
+
constructor(manager) {
|
|
3
|
+
this.manager = manager;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Copy text to clipboard
|
|
7
|
+
clipboardCopy(input) {
|
|
8
|
+
// Get the text from the input
|
|
9
|
+
const text = input && input.nodeType
|
|
10
|
+
? input.value || input.innerText || input.innerHTML
|
|
11
|
+
: input;
|
|
12
|
+
|
|
13
|
+
// Try to use the modern clipboard API
|
|
14
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
15
|
+
return navigator.clipboard.writeText(text).catch(() => {
|
|
16
|
+
fallbackCopy(text);
|
|
17
|
+
});
|
|
18
|
+
} else {
|
|
11
19
|
fallbackCopy(text);
|
|
12
|
-
}
|
|
13
|
-
} else {
|
|
14
|
-
fallbackCopy(text);
|
|
15
|
-
}
|
|
20
|
+
}
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
function fallbackCopy(text) {
|
|
23
|
+
const el = document.createElement('textarea');
|
|
24
|
+
el.setAttribute('style', 'width:1px;border:0;opacity:0;');
|
|
25
|
+
el.value = text;
|
|
26
|
+
document.body.appendChild(el);
|
|
27
|
+
el.select();
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
try {
|
|
30
|
+
document.execCommand('copy');
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error('Failed to copy to clipboard');
|
|
33
|
+
}
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
document.body.removeChild(el);
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
|
-
}
|
|
33
38
|
|
|
34
|
-
// Escape HTML to prevent XSS
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
39
|
+
// Escape HTML to prevent XSS
|
|
40
|
+
escapeHTML(str) {
|
|
41
|
+
if (typeof str !== 'string') {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
this._shadowElement = this._shadowElement || document.createElement('p');
|
|
46
|
+
this._shadowElement.innerHTML = '';
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
// This automatically escapes HTML entities like <, >, &, etc.
|
|
49
|
+
this._shadowElement.appendChild(document.createTextNode(str));
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
51
|
+
// This is needed to escape quotes to prevent attribute injection
|
|
52
|
+
return this._shadowElement.innerHTML.replace(/["']/g, (m) => {
|
|
53
|
+
switch (m) {
|
|
54
|
+
case '"':
|
|
55
|
+
return '"';
|
|
56
|
+
default:
|
|
57
|
+
return ''';
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
57
61
|
|
|
58
|
-
// Show notification
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// Show notification
|
|
63
|
+
showNotification(message, options = {}) {
|
|
64
|
+
// Handle different input types
|
|
65
|
+
let text = message;
|
|
66
|
+
let type = options.type || 'info';
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
// If message is an Error object, extract message and default to danger
|
|
69
|
+
if (message instanceof Error) {
|
|
70
|
+
text = message.message;
|
|
71
|
+
type = options.type || 'danger';
|
|
72
|
+
}
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
// Handle string as second parameter for backwards compatibility
|
|
75
|
+
if (typeof options === 'string') {
|
|
76
|
+
options = { type: options };
|
|
77
|
+
type = options.type;
|
|
78
|
+
}
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
// Extract options
|
|
81
|
+
const timeout = options.timeout !== undefined ? options.timeout : 5000;
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
const $notification = document.createElement('div');
|
|
84
|
+
$notification.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-5`;
|
|
85
|
+
$notification.style.zIndex = '9999';
|
|
86
|
+
$notification.innerHTML = `
|
|
87
|
+
${text}
|
|
88
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
89
|
+
`;
|
|
86
90
|
|
|
87
|
-
|
|
91
|
+
document.body.appendChild($notification);
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
// Auto-remove after timeout (unless timeout is 0)
|
|
94
|
+
if (timeout > 0) {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
$notification.remove();
|
|
97
|
+
}, timeout);
|
|
98
|
+
}
|
|
94
99
|
}
|
|
95
|
-
}
|
|
96
100
|
|
|
97
|
-
// Get platform (OS)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
// Get platform (OS)
|
|
102
|
+
getPlatform() {
|
|
103
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
104
|
+
const platform = (navigator.userAgentData?.platform || navigator.platform || '').toLowerCase();
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
// Check userAgent for mobile platforms (more reliable than platform string)
|
|
107
|
+
if (/iphone|ipad|ipod/.test(ua)) {
|
|
108
|
+
return 'ios';
|
|
109
|
+
}
|
|
110
|
+
if (/android/.test(ua)) {
|
|
111
|
+
return 'android';
|
|
112
|
+
}
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
// Check platform string for desktop OS
|
|
115
|
+
if (/win/.test(platform)) {
|
|
116
|
+
return 'windows';
|
|
117
|
+
}
|
|
118
|
+
if (/mac/.test(platform)) {
|
|
119
|
+
return 'mac';
|
|
120
|
+
}
|
|
121
|
+
if (/cros/.test(ua)) {
|
|
122
|
+
return 'chromeos';
|
|
123
|
+
}
|
|
124
|
+
if (/linux/.test(platform)) {
|
|
125
|
+
return 'linux';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return 'unknown';
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
// Get runtime environment
|
|
132
|
+
getRuntime() {
|
|
133
|
+
// Use config runtime if provided
|
|
134
|
+
if (this.manager?.config?.runtime) {
|
|
135
|
+
return this.manager.config.runtime;
|
|
136
|
+
}
|
|
126
137
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
// Browser extension (Chrome, Edge, Opera, Brave, Firefox, Safari, etc.)
|
|
139
|
+
if (
|
|
140
|
+
(typeof chrome !== 'undefined' && chrome.runtime?.id)
|
|
141
|
+
|| (typeof browser !== 'undefined' && browser.runtime?.id)
|
|
142
|
+
|| (typeof safari !== 'undefined' && safari.extension)
|
|
143
|
+
) {
|
|
144
|
+
return 'browser-extension';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Default: web browser
|
|
148
|
+
return 'web';
|
|
136
149
|
}
|
|
137
150
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// }
|
|
150
|
-
|
|
151
|
-
// Default: web browser
|
|
152
|
-
return 'web';
|
|
153
|
-
}
|
|
151
|
+
// Check if mobile device
|
|
152
|
+
isMobile() {
|
|
153
|
+
try {
|
|
154
|
+
// Try modern API first
|
|
155
|
+
const m = navigator.userAgentData?.mobile;
|
|
156
|
+
if (typeof m !== 'undefined') {
|
|
157
|
+
return m === true;
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// Silent fail
|
|
161
|
+
}
|
|
154
162
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (typeof m !== 'undefined') {
|
|
161
|
-
return m === true;
|
|
163
|
+
// Fallback to media query
|
|
164
|
+
try {
|
|
165
|
+
return window.matchMedia('(max-width: 767px)').matches;
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return false;
|
|
162
168
|
}
|
|
163
|
-
} catch (e) {
|
|
164
|
-
// Silent fail
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
} catch (e) {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
171
|
+
// Get device type based on screen width
|
|
172
|
+
getDeviceType() {
|
|
173
|
+
const width = window.innerWidth;
|
|
174
174
|
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
// Mobile: < 768px (Bootstrap's md breakpoint)
|
|
176
|
+
if (width < 768) {
|
|
177
|
+
return 'mobile';
|
|
178
|
+
}
|
|
178
179
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
// Tablet: 768px - 1199px (between md and xl)
|
|
181
|
+
if (width < 1200) {
|
|
182
|
+
return 'tablet';
|
|
183
|
+
}
|
|
183
184
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
// Desktop: >= 1200px
|
|
186
|
+
return 'desktop';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Get context information
|
|
190
|
+
getContext() {
|
|
191
|
+
// Return context information
|
|
192
|
+
return {
|
|
193
|
+
client: {
|
|
194
|
+
language: navigator.language,
|
|
195
|
+
mobile: this.isMobile(),
|
|
196
|
+
deviceType: this.getDeviceType(),
|
|
197
|
+
platform: this.getPlatform(),
|
|
198
|
+
runtime: this.getRuntime(),
|
|
199
|
+
userAgent: navigator.userAgent,
|
|
200
|
+
url: window.location.href,
|
|
201
|
+
},
|
|
202
|
+
browser: {
|
|
203
|
+
vendor: navigator.vendor,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
187
206
|
}
|
|
188
|
-
|
|
189
|
-
// Desktop: >= 1200px
|
|
190
|
-
return 'desktop';
|
|
191
207
|
}
|
|
192
208
|
|
|
193
|
-
|
|
194
|
-
export function getContext() {
|
|
195
|
-
// Return context information
|
|
196
|
-
return {
|
|
197
|
-
client: {
|
|
198
|
-
language: navigator.language,
|
|
199
|
-
mobile: isMobile(),
|
|
200
|
-
deviceType: getDeviceType(),
|
|
201
|
-
platform: getPlatform(),
|
|
202
|
-
runtime: getRuntime(),
|
|
203
|
-
userAgent: navigator.userAgent,
|
|
204
|
-
url: window.location.href,
|
|
205
|
-
},
|
|
206
|
-
browser: {
|
|
207
|
-
vendor: navigator.vendor,
|
|
208
|
-
},
|
|
209
|
-
// screen: {
|
|
210
|
-
// width: window.screen?.width,
|
|
211
|
-
// height: window.screen?.height,
|
|
212
|
-
// availWidth: window.screen?.availWidth,
|
|
213
|
-
// availHeight: window.screen?.availHeight,
|
|
214
|
-
// colorDepth: window.screen?.colorDepth,
|
|
215
|
-
// pixelRatio: window.devicePixelRatio || 1,
|
|
216
|
-
// },
|
|
217
|
-
// viewport: {
|
|
218
|
-
// width: window.innerWidth,
|
|
219
|
-
// height: window.innerHeight,
|
|
220
|
-
// },
|
|
221
|
-
};
|
|
222
|
-
}
|
|
209
|
+
export default Utilities;
|
package/package.json
CHANGED