-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
248 lines (214 loc) · 8.64 KB
/
Copy pathbackground.js
File metadata and controls
248 lines (214 loc) · 8.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/*
* SenseAudio - Visual Theory & Creative Studio
* Copyright (C) 2026 SensementMusic.com
*
* This file is part of SenseAudio (A Sensement Music Project).
*
* SenseAudio is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SenseAudio is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with SenseAudio. If not, see <https://www.gnu.org/licenses/>.
*
* All branding, logos, and the name "Sensement Music" are properties of SensementMusic.com.
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
/**
* The main production URL of the web application.
* The analytics endpoint is hosted via Cloudflare Pages Functions at /api/analytics.
* This approach avoids hardcoding Worker URLs and keeps the API consistent with the domain.
*/
const APP_DOMAIN = 'https://studio.sensementmusic.com'; // <--- CHANGE THIS TO YOUR REAL DOMAIN
const ANALYTICS_ENDPOINT = `${APP_DOMAIN}/api/analytics`;
/**
* Session timeout threshold in milliseconds.
* 30 minutes of inactivity will result in a new session ID.
*/
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
/**
* Detects the appropriate extension API namespace.
* - 'browser': Standard WebExtensions API (Firefox, Edge, etc.)
* - 'chrome': Chromium-specific API (Chrome, older Edge, Opera)
*/
const globalAPI = (typeof browser !== 'undefined') ? browser : chrome;
// ============================================================================
// 1. App Navigation & Core Logic
// ============================================================================
/**
* Listens for the extension action icon click event.
* Opens the main application interface (index.html) in a new browser tab.
* Supports both Chrome and Firefox (Manifest V3).
*/
if (globalAPI.action && globalAPI.action.onClicked) {
globalAPI.action.onClicked.addListener((tab) => {
globalAPI.tabs.create({
url: globalAPI.runtime.getURL("index.html")
});
});
}
// ============================================================================
// 2. Analytics System (Offline-First via Cloudflare D1)
// ============================================================================
/**
* Retrieves or generates unique identifiers for the user and the current session.
* - Client ID: Persistent ID stored in local storage (simulates a unique device/user).
* - Session ID: Ephemeral ID stored in session storage, resets after inactivity.
* @returns {Promise<{clientId: string, sessionId: string}>} The identity object.
*/
async function getIdentity() {
// 1. Handle Client ID (Persistent)
let data = await globalAPI.storage.local.get('clientId');
let clientId = data.clientId;
if (!clientId) {
clientId = self.crypto.randomUUID();
await globalAPI.storage.local.set({ clientId });
}
// 2. Handle Session ID (Time-bound)
// Note: 'storage.session' requires appropriate permissions in manifest.json
let sessionData = await globalAPI.storage.session.get(['sessionId', 'lastActive']);
let sessionId = sessionData.sessionId;
let lastActive = sessionData.lastActive;
const now = Date.now();
// Check if session is missing or expired
if (!sessionId || !lastActive || (now - lastActive) > SESSION_TIMEOUT_MS) {
sessionId = self.crypto.randomUUID(); // Start new session
}
// Update last activity timestamp
await globalAPI.storage.session.set({ sessionId, lastActive: now });
return { clientId, sessionId };
}
/**
* Main entry point to track an event.
* Handles online/offline states automatically.
* @param {string} eventName - The name of the event (e.g., 'app_startup', 'export_midi').
* @param {Object} [params={}] - Additional metadata for the event.
*/
async function trackEvent(eventName, params = {}) {
try {
const { clientId, sessionId } = await getIdentity();
const timestamp = Date.now();
const payload = {
client_id: clientId,
session_id: sessionId,
events: [{
name: eventName,
params: params,
timestamp: timestamp
}]
};
// Determine dispatch method based on network status
if (navigator.onLine) {
sendData(payload).catch((err) => {
console.warn('[SenseAudio Analytics] Send failed, queuing offline.', err);
saveOffline(payload);
});
} else {
saveOffline(payload);
}
} catch (error) {
console.error('[SenseAudio Analytics] Error in tracking:', error);
}
}
// ----------------------------------------------------------------------------
// Network & Storage Utilities
// ----------------------------------------------------------------------------
/**
* Sends the event payload to the Cloudflare Functions Endpoint.
* @param {Object} payload - The data object containing client info and events.
* @returns {Promise<void>}
*/
async function sendData(payload) {
// Uses the dynamic ANALYTICS_ENDPOINT derived from APP_DOMAIN
const response = await fetch(ANALYTICS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`Server responded with status: ${response.status}`);
}
// On success, attempt to flush any previously queued offline events
flushOfflineQueue();
}
/**
* Saves the event payload to local storage when the user is offline.
* @param {Object} payload - The data object to queue.
*/
async function saveOffline(payload) {
const data = await globalAPI.storage.local.get('offlineQueue');
const offlineQueue = data.offlineQueue || [];
offlineQueue.push(payload);
await globalAPI.storage.local.set({ offlineQueue });
console.debug('[SenseAudio Analytics] Event queued offline.');
}
/**
* Attempts to send all queued offline events to the server.
* Should be called when connectivity is restored.
*/
async function flushOfflineQueue() {
const data = await globalAPI.storage.local.get('offlineQueue');
const offlineQueue = data.offlineQueue || [];
if (offlineQueue.length === 0) return;
console.debug(`[SenseAudio Analytics] Flushing ${offlineQueue.length} offline events...`);
const newQueue = [];
// Process queue items
for (const item of offlineQueue) {
try {
await fetch(ANALYTICS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
});
} catch (e) {
// If transmission fails again, keep it in the queue for next time
newQueue.push(item);
}
}
// Update the queue with any remaining items
await globalAPI.storage.local.set({ offlineQueue: newQueue });
}
// ----------------------------------------------------------------------------
// Event Listeners
// ----------------------------------------------------------------------------
/**
* Triggered when the extension is installed or updated.
*/
globalAPI.runtime.onInstalled.addListener((details) => {
trackEvent('extension_installed', { reason: details.reason });
});
/**
* Triggered when the browser starts up.
* Also acts as a "Wake Up" call to flush offline queues.
*/
globalAPI.runtime.onStartup.addListener(() => {
trackEvent('browser_startup');
// Delay flush to ensure network is initialized
setTimeout(flushOfflineQueue, 5000);
});
/**
* Listens for messages from the main application (UI scripts).
* This allows the front-end to log events via the service worker.
* Message Format: { type: 'ANALYTICS_EVENT', name: '...', params: {...} }
*/
globalAPI.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'ANALYTICS_EVENT') {
trackEvent(request.name, request.params);
}
// Return true if you plan to send a response asynchronously (optional here)
});
/**
* Listens for network restoration to immediately flush the offline queue.
* Note: 'online' event availability in Service Workers depends on browser version.
*/
self.addEventListener('online', () => {
flushOfflineQueue();
});