web-manager 4.3.1 → 4.3.3
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 +4 -0
- package/docs/bindings.md +223 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/modules.md +1 -0
- package/package.json +9 -3
- package/src/index.js +702 -0
- package/src/modules/analytics.js +250 -0
- package/src/modules/auth.js +420 -0
- package/src/modules/bindings.js +314 -0
- package/src/modules/dom.js +96 -0
- package/src/modules/firestore.js +306 -0
- package/src/modules/notifications.js +391 -0
- package/src/modules/sentry.js +199 -0
- package/src/modules/service-worker.js +196 -0
- package/src/modules/storage.js +133 -0
- package/src/modules/usage.js +264 -0
- package/src/modules/utilities.js +310 -0
- package/CHANGELOG.md +0 -233
- package/TODO.md +0 -54
- package/_legacy/helpers/auth-pages.js +0 -24
- package/_legacy/index.js +0 -1854
- package/_legacy/lib/account.js +0 -666
- package/_legacy/lib/debug.js +0 -56
- package/_legacy/lib/dom.js +0 -351
- package/_legacy/lib/require.js +0 -5
- package/_legacy/lib/storage.js +0 -78
- package/_legacy/lib/utilities.js +0 -180
- package/_legacy/service-worker copy.js +0 -347
- package/_legacy/test/test.js +0 -158
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
class Notifications {
|
|
2
|
+
constructor(manager) {
|
|
3
|
+
this.manager = manager;
|
|
4
|
+
this._requestInProgress = false;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
initialize(config) {
|
|
8
|
+
this._vapidKey = this.manager.config?.firebase?.messaging?.config?.vapidKey || null;
|
|
9
|
+
|
|
10
|
+
const storage = this.manager.storage();
|
|
11
|
+
const stored = storage.get('notifications');
|
|
12
|
+
const permission = typeof Notification !== 'undefined' ? Notification.permission : 'default';
|
|
13
|
+
|
|
14
|
+
console.log('[WM:push] Page load check:', { storedSubscribed: stored?.subscribed, storedToken: stored?.token?.slice(-8), permission });
|
|
15
|
+
|
|
16
|
+
// If localStorage says subscribed but browser permission disagrees, clear it
|
|
17
|
+
if (stored?.subscribed && permission !== 'granted') {
|
|
18
|
+
console.log('[WM:push] Clearing stale subscription — permission is', permission);
|
|
19
|
+
storage.set('notifications', { subscribed: false, token: null });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Arm auto-request if not currently subscribed (including just-cleared)
|
|
23
|
+
const autoRequest = config?.autoRequest;
|
|
24
|
+
if ((!stored?.subscribed || permission !== 'granted') && autoRequest > 0) {
|
|
25
|
+
console.log('[WM:push] Arming auto-request (delay:', autoRequest + 'ms)');
|
|
26
|
+
this._setupAutoRequest(autoRequest);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Listen for foreground messages (tab is focused)
|
|
30
|
+
if (permission === 'granted') {
|
|
31
|
+
console.log('[WM:push] Setting up foreground listener...', { supported: this.isSupported(), hasMessaging: !!this.manager.firebaseMessaging });
|
|
32
|
+
this.onMessage((payload) => {
|
|
33
|
+
console.log('[WM:push] Foreground message received:', payload);
|
|
34
|
+
}).then(unsub => {
|
|
35
|
+
console.log('[WM:push] Foreground listener registered:', typeof unsub === 'function' ? 'OK' : 'FAILED (got empty fn)');
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_setupAutoRequest(delay) {
|
|
41
|
+
if (typeof document === 'undefined') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const handleClick = () => {
|
|
46
|
+
document.removeEventListener('click', handleClick);
|
|
47
|
+
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
console.log('[WM:push] Auto-requesting notification permissions...');
|
|
50
|
+
this.subscribe().catch(err => {
|
|
51
|
+
console.error('[WM:push] Auto-subscription failed:', err.message);
|
|
52
|
+
});
|
|
53
|
+
}, delay);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
document.addEventListener('click', handleClick);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if notifications are supported
|
|
60
|
+
isSupported() {
|
|
61
|
+
return 'Notification' in window &&
|
|
62
|
+
'serviceWorker' in navigator &&
|
|
63
|
+
this.manager.firebaseMessaging !== undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if user is subscribed to notifications
|
|
67
|
+
async isSubscribed() {
|
|
68
|
+
try {
|
|
69
|
+
if (!this.isSupported()) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return Notification.permission === 'granted';
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Check subscription error:', error);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Subscribe to push notifications
|
|
81
|
+
async subscribe(options = {}) {
|
|
82
|
+
try {
|
|
83
|
+
if (!this.isSupported()) {
|
|
84
|
+
throw new Error('Push notifications are not supported');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (this._requestInProgress) {
|
|
88
|
+
throw new Error('Subscription request already in progress');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this._requestInProgress = true;
|
|
92
|
+
|
|
93
|
+
// Get Firebase messaging
|
|
94
|
+
const messaging = this.manager.firebaseMessaging;
|
|
95
|
+
if (!messaging) {
|
|
96
|
+
throw new Error('Firebase Messaging not initialized');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Get service worker registration
|
|
100
|
+
const swRegistration = this.manager.state.serviceWorker;
|
|
101
|
+
if (!swRegistration) {
|
|
102
|
+
throw new Error('Service Worker not registered');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Request notification permission if not already granted
|
|
106
|
+
if (Notification.permission === 'default') {
|
|
107
|
+
const permission = await Notification.requestPermission();
|
|
108
|
+
if (permission !== 'granted') {
|
|
109
|
+
throw new Error('Notification permission denied');
|
|
110
|
+
}
|
|
111
|
+
} else if (Notification.permission === 'denied') {
|
|
112
|
+
throw new Error('Notification permission denied');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Get FCM token
|
|
116
|
+
const { getToken } = await import('firebase/messaging');
|
|
117
|
+
const tokenOptions = { serviceWorkerRegistration: swRegistration };
|
|
118
|
+
if (this._vapidKey) { tokenOptions.vapidKey = this._vapidKey; }
|
|
119
|
+
const token = await getToken(messaging, tokenOptions);
|
|
120
|
+
|
|
121
|
+
if (!token) {
|
|
122
|
+
throw new Error('Failed to get FCM token');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Save subscription info
|
|
126
|
+
await this._saveSubscription(token);
|
|
127
|
+
|
|
128
|
+
// Track in local storage
|
|
129
|
+
const storage = this.manager.storage();
|
|
130
|
+
storage.set('notifications', {
|
|
131
|
+
subscribed: true,
|
|
132
|
+
token: token,
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
uid: this.manager.auth().getUser()?.uid || null
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
this._requestInProgress = false;
|
|
138
|
+
return { subscribed: true, token };
|
|
139
|
+
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this._requestInProgress = false;
|
|
142
|
+
console.error('Subscribe error:', error);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Unsubscribe from push notifications
|
|
148
|
+
async unsubscribe() {
|
|
149
|
+
try {
|
|
150
|
+
if (!this.isSupported()) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const { deleteToken } = await import('firebase/messaging');
|
|
155
|
+
const messaging = this.manager.firebaseMessaging;
|
|
156
|
+
|
|
157
|
+
if (messaging) {
|
|
158
|
+
await deleteToken(messaging);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Clear local storage
|
|
162
|
+
const storage = this.manager.storage();
|
|
163
|
+
storage.remove('notifications');
|
|
164
|
+
|
|
165
|
+
return true;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Unsubscribe error:', error);
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Request permission (without subscribing)
|
|
173
|
+
async requestPermission() {
|
|
174
|
+
try {
|
|
175
|
+
if (!this.isSupported()) {
|
|
176
|
+
throw new Error('Notifications not supported');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const permission = await Notification.requestPermission();
|
|
180
|
+
return permission === 'granted';
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('Request permission error:', error);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Get current FCM token
|
|
188
|
+
async getToken() {
|
|
189
|
+
try {
|
|
190
|
+
if (!this.isSupported()) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const messaging = this.manager.firebaseMessaging;
|
|
195
|
+
if (!messaging) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { getToken } = await import('firebase/messaging');
|
|
200
|
+
const swRegistration = this.manager.state.serviceWorker;
|
|
201
|
+
|
|
202
|
+
const tokenOptions = { serviceWorkerRegistration: swRegistration };
|
|
203
|
+
if (this._vapidKey) { tokenOptions.vapidKey = this._vapidKey; }
|
|
204
|
+
return await getToken(messaging, tokenOptions);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('Get token error:', error);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Listen for foreground messages
|
|
212
|
+
async onMessage(callback) {
|
|
213
|
+
try {
|
|
214
|
+
if (!this.isSupported()) {
|
|
215
|
+
return () => {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const { onMessage } = await import('firebase/messaging');
|
|
219
|
+
const messaging = this.manager.firebaseMessaging;
|
|
220
|
+
|
|
221
|
+
if (!messaging) {
|
|
222
|
+
return () => {};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return onMessage(messaging, (payload) => {
|
|
226
|
+
console.log('Foreground message received:', payload);
|
|
227
|
+
|
|
228
|
+
// Extract notification data - handle both payload.notification and payload.data formats
|
|
229
|
+
const notificationData = payload.notification || payload.data || {};
|
|
230
|
+
const { title, body, icon, badge, image, click_action, url, tag } = notificationData;
|
|
231
|
+
|
|
232
|
+
// Determine the click URL (prioritize click_action, then url, then data.url)
|
|
233
|
+
const clickUrl = click_action || url || payload.data?.click_action || payload.data?.url;
|
|
234
|
+
|
|
235
|
+
// Show notification if we have at least a title
|
|
236
|
+
if (title) {
|
|
237
|
+
const notification = new Notification(title, {
|
|
238
|
+
body: body || '',
|
|
239
|
+
icon: icon || '/favicon.ico',
|
|
240
|
+
badge: badge,
|
|
241
|
+
image: image,
|
|
242
|
+
tag: tag || 'default',
|
|
243
|
+
data: { ...payload.data, clickUrl },
|
|
244
|
+
requireInteraction: true,
|
|
245
|
+
renotify: true
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
notification.onclick = (event) => {
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
|
|
251
|
+
// Focus or open the target window
|
|
252
|
+
if (clickUrl) {
|
|
253
|
+
// Try to find an existing window/tab with this URL
|
|
254
|
+
window.focus();
|
|
255
|
+
window.open(clickUrl, '_blank');
|
|
256
|
+
} else {
|
|
257
|
+
// Just focus the current window if no URL
|
|
258
|
+
window.focus();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
notification.close();
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Call the user's callback with the full payload
|
|
266
|
+
if (callback) {
|
|
267
|
+
callback(payload);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error('Message listener error:', error);
|
|
272
|
+
return () => {};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Sync subscription when auth state changes or on page load.
|
|
277
|
+
// Re-fetches the current FCM token — if it changed (browser rotated it,
|
|
278
|
+
// service worker was re-registered, etc.), saves the new token to Firestore
|
|
279
|
+
// and updates localStorage. Clears the subscribed state if the token is gone.
|
|
280
|
+
async syncSubscription() {
|
|
281
|
+
try {
|
|
282
|
+
const storage = this.manager.storage();
|
|
283
|
+
const storedNotification = storage.get('notifications');
|
|
284
|
+
|
|
285
|
+
const permission = typeof Notification !== 'undefined' ? Notification.permission : 'default';
|
|
286
|
+
|
|
287
|
+
console.log('[WM:push:sync] Starting sync:', { storedSubscribed: storedNotification?.subscribed, storedToken: storedNotification?.token?.slice(-8), permission });
|
|
288
|
+
|
|
289
|
+
if (permission !== 'granted') {
|
|
290
|
+
if (storedNotification?.subscribed) {
|
|
291
|
+
console.log('[WM:push:sync] Permission not granted — clearing localStorage');
|
|
292
|
+
storage.set('notifications', { subscribed: false, token: null });
|
|
293
|
+
} else {
|
|
294
|
+
console.log('[WM:push:sync] Permission not granted and not subscribed — nothing to do');
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Permission is granted — check if there's a live token (covers localStorage cleared, new browser, etc.)
|
|
300
|
+
const currentToken = await this.getToken();
|
|
301
|
+
|
|
302
|
+
if (!currentToken) {
|
|
303
|
+
console.log('[WM:push:sync] Token fetch returned null — clearing localStorage');
|
|
304
|
+
storage.set('notifications', { subscribed: false, token: null });
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log('[WM:push:sync] Token valid:', currentToken.slice(-8), storedNotification?.token ? (storedNotification.token.slice(-8) === currentToken.slice(-8) ? '(unchanged)' : '(CHANGED from ' + storedNotification.token.slice(-8) + ')') : '(recovered — localStorage was empty)');
|
|
309
|
+
|
|
310
|
+
await this._saveSubscription(currentToken);
|
|
311
|
+
|
|
312
|
+
const user = this.manager.auth().getUser();
|
|
313
|
+
storage.set('notifications', {
|
|
314
|
+
subscribed: true,
|
|
315
|
+
token: currentToken,
|
|
316
|
+
uid: user?.uid || null,
|
|
317
|
+
timestamp: new Date().toISOString(),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
console.log('[WM:push:sync] Sync complete — subscribed');
|
|
321
|
+
return true;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error('[WM:push:sync] Sync error:', error);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Save subscription to Firestore
|
|
329
|
+
async _saveSubscription(token) {
|
|
330
|
+
try {
|
|
331
|
+
const firestore = this.manager.firestore();
|
|
332
|
+
const user = this.manager.auth().getUser();
|
|
333
|
+
const storage = this.manager.storage();
|
|
334
|
+
|
|
335
|
+
if (!token) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const now = new Date();
|
|
340
|
+
const timestamp = now.toISOString();
|
|
341
|
+
const timestampUNIX = Math.floor(now.getTime() / 1000);
|
|
342
|
+
|
|
343
|
+
// Get context for client information
|
|
344
|
+
const context = this.manager.utilities().getContext();
|
|
345
|
+
const clientData = context.client;
|
|
346
|
+
|
|
347
|
+
// Reference to the notification document (ID is the token)
|
|
348
|
+
const notificationDoc = firestore.doc(`notifications/${token}`);
|
|
349
|
+
|
|
350
|
+
// Check if document already exists
|
|
351
|
+
const existingDoc = await notificationDoc.get();
|
|
352
|
+
const existingData = existingDoc.exists() ? existingDoc.data() : null;
|
|
353
|
+
|
|
354
|
+
// Determine if we need to update
|
|
355
|
+
const currentUid = user?.uid || null;
|
|
356
|
+
const existingOwner = existingData?.owner || null;
|
|
357
|
+
const needsUpdate = existingOwner !== currentUid;
|
|
358
|
+
|
|
359
|
+
// Create or update the document as needed
|
|
360
|
+
if (!existingData) {
|
|
361
|
+
// New subscription - create the document
|
|
362
|
+
await notificationDoc.set({
|
|
363
|
+
token,
|
|
364
|
+
owner: currentUid,
|
|
365
|
+
tags: ['general'],
|
|
366
|
+
attribution: storage.get('attribution', {}),
|
|
367
|
+
context: { client: clientData },
|
|
368
|
+
metadata: {
|
|
369
|
+
created: { timestamp, timestampUNIX },
|
|
370
|
+
updated: { timestamp, timestampUNIX },
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
} else if (needsUpdate) {
|
|
374
|
+
// Existing subscription needs update (userId changed)
|
|
375
|
+
// Use dot-notation to avoid overwriting metadata.created
|
|
376
|
+
await notificationDoc.update({
|
|
377
|
+
owner: currentUid,
|
|
378
|
+
context: { client: clientData },
|
|
379
|
+
'metadata.updated': { timestamp, timestampUNIX },
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// If no update needed, do nothing
|
|
383
|
+
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error('Save subscription error:', error);
|
|
386
|
+
// Don't throw - this is not critical for the subscription process
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export default Notifications;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Helper functions
|
|
2
|
+
function isLighthouse() {
|
|
3
|
+
try {
|
|
4
|
+
return typeof navigator !== 'undefined' && navigator.userAgent?.includes('Lighthouse');
|
|
5
|
+
} catch (e) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isAutomatedBrowser() {
|
|
11
|
+
try {
|
|
12
|
+
return typeof navigator !== 'undefined' && navigator.webdriver === true;
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class mod {
|
|
19
|
+
constructor(manager) {
|
|
20
|
+
this.manager = manager;
|
|
21
|
+
this.initialized = false;
|
|
22
|
+
this.Sentry = null;
|
|
23
|
+
this.config = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize Sentry error tracking
|
|
28
|
+
* @param {Object} config - Sentry configuration object
|
|
29
|
+
* @returns {Promise} Resolves when initialization is complete
|
|
30
|
+
*/
|
|
31
|
+
init(config = {}) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
// Dynamically import Sentry to reduce initial bundle size
|
|
34
|
+
import('@sentry/browser')
|
|
35
|
+
.then((mod) => {
|
|
36
|
+
// Store reference and expose globally
|
|
37
|
+
this.Sentry = mod;
|
|
38
|
+
|
|
39
|
+
// Add to window if window is defined
|
|
40
|
+
if (typeof window !== 'undefined') {
|
|
41
|
+
window.Sentry = mod;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Build configuration with our defaults
|
|
45
|
+
this.config = this._buildConfig(config);
|
|
46
|
+
|
|
47
|
+
// Initialize Sentry
|
|
48
|
+
this.Sentry.init(this.config);
|
|
49
|
+
this.initialized = true;
|
|
50
|
+
|
|
51
|
+
resolve({ initialized: true });
|
|
52
|
+
})
|
|
53
|
+
.catch((error) => {
|
|
54
|
+
console.error('[Sentry] Failed to initialize:', error);
|
|
55
|
+
reject(error);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build Sentry configuration with defaults and integrations
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
_buildConfig(userConfig) {
|
|
65
|
+
const config = { ...userConfig };
|
|
66
|
+
const manager = this.manager;
|
|
67
|
+
|
|
68
|
+
// Set release version and environment
|
|
69
|
+
config.release = `${manager.config.brand.id}@${manager.config.buildTime}`;
|
|
70
|
+
config.environment = manager.config.environment || 'production';
|
|
71
|
+
config.integrations = config.integrations || [];
|
|
72
|
+
|
|
73
|
+
// Add browser tracing integration if not already present
|
|
74
|
+
if (!config.integrations.some(i => i.name === 'BrowserTracing')) {
|
|
75
|
+
config.integrations.push(this.Sentry.browserTracingIntegration());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add replay integration if sample rates are configured
|
|
79
|
+
const hasReplays = (config.replaysSessionSampleRate > 0) ||
|
|
80
|
+
(config.replaysOnErrorSampleRate > 0);
|
|
81
|
+
|
|
82
|
+
if (hasReplays && !config.integrations.some(i => i.name === 'Replay')) {
|
|
83
|
+
config.integrations.push(this.Sentry.replayIntegration({
|
|
84
|
+
maskAllText: false,
|
|
85
|
+
blockAllMedia: false,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Configure beforeSend to enrich events with user data and session info
|
|
90
|
+
config.beforeSend = (event, hint) => {
|
|
91
|
+
const startTime = this.manager.config.page.startTime || Date.now();
|
|
92
|
+
const hoursSinceStart = (Date.now() - startTime) / (1000 * 3600);
|
|
93
|
+
const storage = this.manager.storage();
|
|
94
|
+
|
|
95
|
+
// Add custom tags
|
|
96
|
+
event.tags = {
|
|
97
|
+
...event.tags,
|
|
98
|
+
'process.type': 'browser',
|
|
99
|
+
'usage.session.hours': hoursSinceStart.toFixed(2),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Add user info from storage
|
|
103
|
+
event.user = {
|
|
104
|
+
...event.user,
|
|
105
|
+
email: storage.get('user.auth.email', ''),
|
|
106
|
+
uid: storage.get('user.auth.uid', ''),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Log error to console for debugging
|
|
110
|
+
console.error('[Sentry] Caught error:', {
|
|
111
|
+
message: event.message || (event.exception?.values?.[0]?.value) || 'Unknown error',
|
|
112
|
+
level: event.level,
|
|
113
|
+
tags: event.tags,
|
|
114
|
+
user: event.user,
|
|
115
|
+
hint
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Block sending in development mode
|
|
119
|
+
if (this.manager.isDevelopment()) {
|
|
120
|
+
console.log('[Sentry] Development mode - not sending to Sentry');
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Block sending if Lighthouse is running
|
|
125
|
+
if (isLighthouse()) {
|
|
126
|
+
console.log('[Sentry] Lighthouse detected - not sending to Sentry');
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Block sending if automated browser (Selenium, Puppeteer, etc.)
|
|
131
|
+
if (isAutomatedBrowser()) {
|
|
132
|
+
console.log('[Sentry] Automated browser detected - not sending to Sentry');
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return event;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return config;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Capture an exception and send to Sentry
|
|
144
|
+
* Safe to call even if Sentry is not initialized
|
|
145
|
+
* @param {Error} error - The error to capture
|
|
146
|
+
* @param {Object} captureContext - Additional context for the error
|
|
147
|
+
* @returns {string|null} Event ID if successful, null otherwise
|
|
148
|
+
*/
|
|
149
|
+
captureException(error, captureContext) {
|
|
150
|
+
// Log the error
|
|
151
|
+
console.error('[Sentry] Capturing exception:', error);
|
|
152
|
+
|
|
153
|
+
// Safe to call - won't throw if not initialized
|
|
154
|
+
if (!this.initialized) {
|
|
155
|
+
console.log('[Sentry] Not initialized, skipping capture');
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Call Sentry to capture the exception
|
|
160
|
+
try {
|
|
161
|
+
return this.Sentry.captureException(error, captureContext);
|
|
162
|
+
} catch (captureError) {
|
|
163
|
+
console.error('[Sentry] Failed to capture exception:', captureError);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// /**
|
|
169
|
+
// * Capture a message and send to Sentry
|
|
170
|
+
// * Safe to call even if Sentry is not initialized
|
|
171
|
+
// * @param {string} message - The message to capture
|
|
172
|
+
// * @param {string} level - Severity level (debug, info, warning, error, fatal)
|
|
173
|
+
// * @param {Object} captureContext - Additional context for the message
|
|
174
|
+
// * @returns {string|null} Event ID if successful, null otherwise
|
|
175
|
+
// */
|
|
176
|
+
// captureMessage(message, level = 'info', captureContext) {
|
|
177
|
+
// if (!message) {
|
|
178
|
+
// console.warn('[Sentry] captureMessage called with no message');
|
|
179
|
+
// return null;
|
|
180
|
+
// }
|
|
181
|
+
|
|
182
|
+
// console.log(`[Sentry] Capturing message (${level}):`, message);
|
|
183
|
+
|
|
184
|
+
// // Safe to call - won't throw if not initialized
|
|
185
|
+
// if (!this.initialized || !this.Sentry) {
|
|
186
|
+
// console.log('[Sentry] Not initialized, skipping capture');
|
|
187
|
+
// return null;
|
|
188
|
+
// }
|
|
189
|
+
|
|
190
|
+
// try {
|
|
191
|
+
// return this.Sentry.captureMessage(message, level, captureContext);
|
|
192
|
+
// } catch (captureError) {
|
|
193
|
+
// console.error('[Sentry] Failed to capture message:', captureError);
|
|
194
|
+
// return null;
|
|
195
|
+
// }
|
|
196
|
+
// }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default mod;
|