web-manager 3.2.74 → 4.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/CLAUDE.md +42 -0
- package/README.md +63 -0
- package/TODO.md +14 -0
- package/_legacy/test/test.js +158 -0
- package/dist/index.js +540 -0
- package/dist/modules/auth.js +256 -0
- package/dist/modules/bindings.js +183 -0
- package/dist/modules/dom.js +102 -0
- package/dist/modules/firestore.js +242 -0
- package/dist/modules/notifications.js +285 -0
- package/dist/modules/sentry.js +166 -0
- package/dist/modules/service-worker.js +321 -0
- package/dist/modules/storage.js +132 -0
- package/dist/modules/utilities.js +143 -0
- package/package.json +9 -8
- /package/{helpers → _legacy/helpers}/auth-pages.js +0 -0
- /package/{index.js → _legacy/index.js} +0 -0
- /package/{lib → _legacy/lib}/account.js +0 -0
- /package/{lib → _legacy/lib}/debug.js +0 -0
- /package/{lib → _legacy/lib}/dom.js +0 -0
- /package/{lib → _legacy/lib}/require.js +0 -0
- /package/{lib → _legacy/lib}/storage.js +0 -0
- /package/{lib → _legacy/lib}/utilities.js +0 -0
@@ -0,0 +1,242 @@
|
|
1
|
+
class Firestore {
|
2
|
+
constructor(manager) {
|
3
|
+
this.manager = manager;
|
4
|
+
this._db = null;
|
5
|
+
this._initialized = false;
|
6
|
+
this._initPromise = null;
|
7
|
+
}
|
8
|
+
|
9
|
+
async _ensureInitialized() {
|
10
|
+
if (this._initialized) {
|
11
|
+
return this._db;
|
12
|
+
}
|
13
|
+
|
14
|
+
if (!this._initPromise) {
|
15
|
+
this._initPromise = this._initializeFirestore();
|
16
|
+
}
|
17
|
+
|
18
|
+
return this._initPromise;
|
19
|
+
}
|
20
|
+
|
21
|
+
async _initializeFirestore() {
|
22
|
+
try {
|
23
|
+
// Check if Firebase app is initialized
|
24
|
+
if (!this.manager._firebaseApp) {
|
25
|
+
throw new Error('Firebase app not initialized. Please initialize Firebase first.');
|
26
|
+
}
|
27
|
+
|
28
|
+
// Dynamically import Firestore
|
29
|
+
const { getFirestore, doc: firestoreDoc, collection: firestoreCollection, getDoc, setDoc, updateDoc, deleteDoc, getDocs, query, where, orderBy, limit, startAt, endAt } = await import('firebase/firestore');
|
30
|
+
|
31
|
+
// Store references for later use
|
32
|
+
this._firestoreMethods = {
|
33
|
+
getFirestore,
|
34
|
+
doc: firestoreDoc,
|
35
|
+
collection: firestoreCollection,
|
36
|
+
getDoc,
|
37
|
+
setDoc,
|
38
|
+
updateDoc,
|
39
|
+
deleteDoc,
|
40
|
+
getDocs,
|
41
|
+
query,
|
42
|
+
where,
|
43
|
+
orderBy,
|
44
|
+
limit,
|
45
|
+
startAt,
|
46
|
+
endAt
|
47
|
+
};
|
48
|
+
|
49
|
+
// Initialize Firestore
|
50
|
+
this._db = getFirestore(this.manager._firebaseApp);
|
51
|
+
this._initialized = true;
|
52
|
+
|
53
|
+
return this._db;
|
54
|
+
} catch (error) {
|
55
|
+
console.error('Failed to initialize Firestore:', error);
|
56
|
+
throw error;
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
// Main doc() method - supports both 'path/to/doc' and ('collection', 'docId')
|
61
|
+
doc(...args) {
|
62
|
+
const self = this;
|
63
|
+
let docPath;
|
64
|
+
|
65
|
+
// Handle different argument patterns
|
66
|
+
if (args.length === 1 && typeof args[0] === 'string') {
|
67
|
+
// Single path string: doc('users/userId')
|
68
|
+
docPath = args[0];
|
69
|
+
} else if (args.length === 2 && typeof args[0] === 'string' && typeof args[1] === 'string') {
|
70
|
+
// Collection and doc ID: doc('users', 'userId')
|
71
|
+
docPath = `${args[0]}/${args[1]}`;
|
72
|
+
} else {
|
73
|
+
throw new Error('Invalid arguments for doc(). Use doc("path/to/doc") or doc("collection", "docId")');
|
74
|
+
}
|
75
|
+
|
76
|
+
return {
|
77
|
+
async get() {
|
78
|
+
await self._ensureInitialized();
|
79
|
+
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
80
|
+
const docSnap = await self._firestoreMethods.getDoc(docRef);
|
81
|
+
|
82
|
+
return {
|
83
|
+
exists: () => docSnap.exists(),
|
84
|
+
data: () => docSnap.data(),
|
85
|
+
id: docSnap.id,
|
86
|
+
ref: docRef
|
87
|
+
};
|
88
|
+
},
|
89
|
+
|
90
|
+
async set(data, options = {}) {
|
91
|
+
await self._ensureInitialized();
|
92
|
+
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
93
|
+
return await self._firestoreMethods.setDoc(docRef, data, options);
|
94
|
+
},
|
95
|
+
|
96
|
+
async update(data) {
|
97
|
+
await self._ensureInitialized();
|
98
|
+
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
99
|
+
return await self._firestoreMethods.updateDoc(docRef, data);
|
100
|
+
},
|
101
|
+
|
102
|
+
async delete() {
|
103
|
+
await self._ensureInitialized();
|
104
|
+
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
105
|
+
return await self._firestoreMethods.deleteDoc(docRef);
|
106
|
+
}
|
107
|
+
};
|
108
|
+
}
|
109
|
+
|
110
|
+
// Collection method for queries
|
111
|
+
collection(collectionPath) {
|
112
|
+
const self = this;
|
113
|
+
|
114
|
+
return {
|
115
|
+
async get() {
|
116
|
+
await self._ensureInitialized();
|
117
|
+
const collRef = self._firestoreMethods.collection(self._db, collectionPath);
|
118
|
+
const querySnapshot = await self._firestoreMethods.getDocs(collRef);
|
119
|
+
|
120
|
+
return {
|
121
|
+
docs: querySnapshot.docs.map(doc => ({
|
122
|
+
id: doc.id,
|
123
|
+
data: () => doc.data(),
|
124
|
+
exists: () => doc.exists(),
|
125
|
+
ref: doc.ref
|
126
|
+
})),
|
127
|
+
size: querySnapshot.size,
|
128
|
+
empty: querySnapshot.empty,
|
129
|
+
forEach: (callback) => querySnapshot.forEach(callback)
|
130
|
+
};
|
131
|
+
},
|
132
|
+
|
133
|
+
where(field, operator, value) {
|
134
|
+
return self._buildQuery(collectionPath, [{ type: 'where', field, operator, value }]);
|
135
|
+
},
|
136
|
+
|
137
|
+
orderBy(field, direction = 'asc') {
|
138
|
+
return self._buildQuery(collectionPath, [{ type: 'orderBy', field, direction }]);
|
139
|
+
},
|
140
|
+
|
141
|
+
limit(count) {
|
142
|
+
return self._buildQuery(collectionPath, [{ type: 'limit', count }]);
|
143
|
+
},
|
144
|
+
|
145
|
+
doc(docId) {
|
146
|
+
if (docId) {
|
147
|
+
return self.doc(`${collectionPath}/${docId}`);
|
148
|
+
}
|
149
|
+
// Auto-generate ID if not provided
|
150
|
+
return self.doc(`${collectionPath}/${self._generateId()}`);
|
151
|
+
}
|
152
|
+
};
|
153
|
+
}
|
154
|
+
|
155
|
+
// Build chainable query
|
156
|
+
_buildQuery(collectionPath, constraints = []) {
|
157
|
+
const self = this;
|
158
|
+
|
159
|
+
const queryBuilder = {
|
160
|
+
where(field, operator, value) {
|
161
|
+
constraints.push({ type: 'where', field, operator, value });
|
162
|
+
return queryBuilder;
|
163
|
+
},
|
164
|
+
|
165
|
+
orderBy(field, direction = 'asc') {
|
166
|
+
constraints.push({ type: 'orderBy', field, direction });
|
167
|
+
return queryBuilder;
|
168
|
+
},
|
169
|
+
|
170
|
+
limit(count) {
|
171
|
+
constraints.push({ type: 'limit', count });
|
172
|
+
return queryBuilder;
|
173
|
+
},
|
174
|
+
|
175
|
+
startAt(...values) {
|
176
|
+
constraints.push({ type: 'startAt', values });
|
177
|
+
return queryBuilder;
|
178
|
+
},
|
179
|
+
|
180
|
+
endAt(...values) {
|
181
|
+
constraints.push({ type: 'endAt', values });
|
182
|
+
return queryBuilder;
|
183
|
+
},
|
184
|
+
|
185
|
+
async get() {
|
186
|
+
await self._ensureInitialized();
|
187
|
+
const collRef = self._firestoreMethods.collection(self._db, collectionPath);
|
188
|
+
|
189
|
+
// Build query constraints
|
190
|
+
const queryConstraints = [];
|
191
|
+
for (const constraint of constraints) {
|
192
|
+
switch (constraint.type) {
|
193
|
+
case 'where':
|
194
|
+
queryConstraints.push(self._firestoreMethods.where(constraint.field, constraint.operator, constraint.value));
|
195
|
+
break;
|
196
|
+
case 'orderBy':
|
197
|
+
queryConstraints.push(self._firestoreMethods.orderBy(constraint.field, constraint.direction));
|
198
|
+
break;
|
199
|
+
case 'limit':
|
200
|
+
queryConstraints.push(self._firestoreMethods.limit(constraint.count));
|
201
|
+
break;
|
202
|
+
case 'startAt':
|
203
|
+
queryConstraints.push(self._firestoreMethods.startAt(...constraint.values));
|
204
|
+
break;
|
205
|
+
case 'endAt':
|
206
|
+
queryConstraints.push(self._firestoreMethods.endAt(...constraint.values));
|
207
|
+
break;
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
const q = self._firestoreMethods.query(collRef, ...queryConstraints);
|
212
|
+
const querySnapshot = await self._firestoreMethods.getDocs(q);
|
213
|
+
|
214
|
+
return {
|
215
|
+
docs: querySnapshot.docs.map(doc => ({
|
216
|
+
id: doc.id,
|
217
|
+
data: () => doc.data(),
|
218
|
+
exists: () => doc.exists(),
|
219
|
+
ref: doc.ref
|
220
|
+
})),
|
221
|
+
size: querySnapshot.size,
|
222
|
+
empty: querySnapshot.empty,
|
223
|
+
forEach: (callback) => querySnapshot.forEach(callback)
|
224
|
+
};
|
225
|
+
}
|
226
|
+
};
|
227
|
+
|
228
|
+
return queryBuilder;
|
229
|
+
}
|
230
|
+
|
231
|
+
// Helper to generate document IDs (similar to Firebase auto-generated IDs)
|
232
|
+
_generateId() {
|
233
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
234
|
+
let id = '';
|
235
|
+
for (let i = 0; i < 20; i++) {
|
236
|
+
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
237
|
+
}
|
238
|
+
return id;
|
239
|
+
}
|
240
|
+
}
|
241
|
+
|
242
|
+
export default Firestore;
|
@@ -0,0 +1,285 @@
|
|
1
|
+
class Notifications {
|
2
|
+
constructor(manager) {
|
3
|
+
this.manager = manager;
|
4
|
+
this._requestInProgress = false;
|
5
|
+
}
|
6
|
+
|
7
|
+
// Check if notifications are supported
|
8
|
+
isSupported() {
|
9
|
+
return 'Notification' in window &&
|
10
|
+
'serviceWorker' in navigator &&
|
11
|
+
this.manager.firebaseMessaging !== undefined;
|
12
|
+
}
|
13
|
+
|
14
|
+
// Check if user is subscribed to notifications
|
15
|
+
async isSubscribed() {
|
16
|
+
try {
|
17
|
+
if (!this.isSupported()) {
|
18
|
+
return false;
|
19
|
+
}
|
20
|
+
|
21
|
+
return Notification.permission === 'granted';
|
22
|
+
} catch (error) {
|
23
|
+
console.error('Check subscription error:', error);
|
24
|
+
return false;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
// Subscribe to push notifications
|
29
|
+
async subscribe(options = {}) {
|
30
|
+
try {
|
31
|
+
if (!this.isSupported()) {
|
32
|
+
throw new Error('Push notifications are not supported');
|
33
|
+
}
|
34
|
+
|
35
|
+
if (this._requestInProgress) {
|
36
|
+
throw new Error('Subscription request already in progress');
|
37
|
+
}
|
38
|
+
|
39
|
+
this._requestInProgress = true;
|
40
|
+
|
41
|
+
// Get Firebase messaging
|
42
|
+
const messaging = this.manager.firebaseMessaging;
|
43
|
+
if (!messaging) {
|
44
|
+
throw new Error('Firebase Messaging not initialized');
|
45
|
+
}
|
46
|
+
|
47
|
+
// Get service worker registration
|
48
|
+
const swRegistration = this.manager.state.serviceWorker;
|
49
|
+
if (!swRegistration) {
|
50
|
+
throw new Error('Service Worker not registered');
|
51
|
+
}
|
52
|
+
|
53
|
+
// Request notification permission if not already granted
|
54
|
+
if (Notification.permission === 'default') {
|
55
|
+
const permission = await Notification.requestPermission();
|
56
|
+
if (permission !== 'granted') {
|
57
|
+
throw new Error('Notification permission denied');
|
58
|
+
}
|
59
|
+
} else if (Notification.permission === 'denied') {
|
60
|
+
throw new Error('Notification permission denied');
|
61
|
+
}
|
62
|
+
|
63
|
+
// Get FCM token
|
64
|
+
const { getToken } = await import('firebase/messaging');
|
65
|
+
const token = await getToken(messaging, {
|
66
|
+
serviceWorkerRegistration: swRegistration,
|
67
|
+
vapidKey: options.vapidKey
|
68
|
+
});
|
69
|
+
|
70
|
+
if (!token) {
|
71
|
+
throw new Error('Failed to get FCM token');
|
72
|
+
}
|
73
|
+
|
74
|
+
// Save subscription info
|
75
|
+
await this._saveSubscription(token);
|
76
|
+
|
77
|
+
// Track in local storage
|
78
|
+
const storage = this.manager.storage();
|
79
|
+
storage.set('notifications', {
|
80
|
+
subscribed: true,
|
81
|
+
token: token,
|
82
|
+
subscribedAt: new Date().toISOString(),
|
83
|
+
uid: this.manager.auth().getUser()?.uid || null
|
84
|
+
});
|
85
|
+
|
86
|
+
this._requestInProgress = false;
|
87
|
+
return { subscribed: true, token };
|
88
|
+
|
89
|
+
} catch (error) {
|
90
|
+
this._requestInProgress = false;
|
91
|
+
console.error('Subscribe error:', error);
|
92
|
+
throw error;
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
// Unsubscribe from push notifications
|
97
|
+
async unsubscribe() {
|
98
|
+
try {
|
99
|
+
if (!this.isSupported()) {
|
100
|
+
return false;
|
101
|
+
}
|
102
|
+
|
103
|
+
const { deleteToken } = await import('firebase/messaging');
|
104
|
+
const messaging = this.manager.firebaseMessaging;
|
105
|
+
|
106
|
+
if (messaging) {
|
107
|
+
await deleteToken(messaging);
|
108
|
+
}
|
109
|
+
|
110
|
+
// Clear local storage
|
111
|
+
const storage = this.manager.storage();
|
112
|
+
storage.remove('notifications');
|
113
|
+
|
114
|
+
return true;
|
115
|
+
} catch (error) {
|
116
|
+
console.error('Unsubscribe error:', error);
|
117
|
+
throw error;
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
// Request permission (without subscribing)
|
122
|
+
async requestPermission() {
|
123
|
+
try {
|
124
|
+
if (!('Notification' in window)) {
|
125
|
+
throw new Error('Notifications not supported');
|
126
|
+
}
|
127
|
+
|
128
|
+
const permission = await Notification.requestPermission();
|
129
|
+
return permission === 'granted';
|
130
|
+
} catch (error) {
|
131
|
+
console.error('Request permission error:', error);
|
132
|
+
return false;
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
// Get current FCM token
|
137
|
+
async getToken() {
|
138
|
+
try {
|
139
|
+
if (!this.isSupported()) {
|
140
|
+
return null;
|
141
|
+
}
|
142
|
+
|
143
|
+
const messaging = this.manager.firebaseMessaging;
|
144
|
+
if (!messaging) {
|
145
|
+
return null;
|
146
|
+
}
|
147
|
+
|
148
|
+
const { getToken } = await import('firebase/messaging');
|
149
|
+
const swRegistration = this.manager.state.serviceWorker;
|
150
|
+
|
151
|
+
return await getToken(messaging, {
|
152
|
+
serviceWorkerRegistration: swRegistration
|
153
|
+
});
|
154
|
+
} catch (error) {
|
155
|
+
console.error('Get token error:', error);
|
156
|
+
return null;
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
// Listen for token refresh
|
161
|
+
async onTokenRefresh(callback) {
|
162
|
+
try {
|
163
|
+
if (!this.isSupported()) {
|
164
|
+
return () => {};
|
165
|
+
}
|
166
|
+
|
167
|
+
const { onMessage } = await import('firebase/messaging');
|
168
|
+
const messaging = this.manager.firebaseMessaging;
|
169
|
+
|
170
|
+
if (!messaging) {
|
171
|
+
return () => {};
|
172
|
+
}
|
173
|
+
|
174
|
+
return onMessage(messaging, (payload) => {
|
175
|
+
if (payload.data?.refreshToken) {
|
176
|
+
callback(payload.data.token);
|
177
|
+
}
|
178
|
+
});
|
179
|
+
} catch (error) {
|
180
|
+
console.error('Token refresh listener error:', error);
|
181
|
+
return () => {};
|
182
|
+
}
|
183
|
+
}
|
184
|
+
|
185
|
+
// Listen for foreground messages
|
186
|
+
async onMessage(callback) {
|
187
|
+
try {
|
188
|
+
if (!this.isSupported()) {
|
189
|
+
return () => {};
|
190
|
+
}
|
191
|
+
|
192
|
+
const { onMessage } = await import('firebase/messaging');
|
193
|
+
const messaging = this.manager.firebaseMessaging;
|
194
|
+
|
195
|
+
if (!messaging) {
|
196
|
+
return () => {};
|
197
|
+
}
|
198
|
+
|
199
|
+
return onMessage(messaging, (payload) => {
|
200
|
+
console.log('Foreground message received:', payload);
|
201
|
+
|
202
|
+
// Show notification if app is in foreground
|
203
|
+
if (payload.notification) {
|
204
|
+
const { title, body, icon, badge, image } = payload.notification;
|
205
|
+
const clickAction = payload.data?.click_action || payload.notification.click_action;
|
206
|
+
|
207
|
+
const notification = new Notification(title, {
|
208
|
+
body,
|
209
|
+
icon,
|
210
|
+
badge,
|
211
|
+
image,
|
212
|
+
data: payload.data,
|
213
|
+
requireInteraction: false,
|
214
|
+
});
|
215
|
+
|
216
|
+
notification.onclick = (event) => {
|
217
|
+
event.preventDefault();
|
218
|
+
if (clickAction) {
|
219
|
+
window.open(clickAction, '_blank');
|
220
|
+
}
|
221
|
+
notification.close();
|
222
|
+
};
|
223
|
+
}
|
224
|
+
|
225
|
+
callback(payload);
|
226
|
+
});
|
227
|
+
} catch (error) {
|
228
|
+
console.error('Message listener error:', error);
|
229
|
+
return () => {};
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
// Save subscription to Firestore
|
234
|
+
async _saveSubscription(token) {
|
235
|
+
try {
|
236
|
+
const firestore = this.manager.firebaseFirestore;
|
237
|
+
const user = this.manager.auth().getUser();
|
238
|
+
|
239
|
+
if (!firestore || !token) {
|
240
|
+
return;
|
241
|
+
}
|
242
|
+
|
243
|
+
const { doc, setDoc, serverTimestamp } = await import('firebase/firestore');
|
244
|
+
|
245
|
+
const subscriptionData = {
|
246
|
+
token,
|
247
|
+
platform: 'web',
|
248
|
+
userAgent: navigator.userAgent,
|
249
|
+
tags: ['general'],
|
250
|
+
created: serverTimestamp(),
|
251
|
+
updated: serverTimestamp(),
|
252
|
+
};
|
253
|
+
|
254
|
+
if (user) {
|
255
|
+
subscriptionData.userId = user.uid;
|
256
|
+
subscriptionData.userEmail = user.email;
|
257
|
+
}
|
258
|
+
|
259
|
+
// Save to notifications collection
|
260
|
+
await setDoc(
|
261
|
+
doc(firestore, 'notifications', token),
|
262
|
+
subscriptionData,
|
263
|
+
{ merge: true }
|
264
|
+
);
|
265
|
+
|
266
|
+
// If user is authenticated, also save reference in user document
|
267
|
+
if (user) {
|
268
|
+
await setDoc(
|
269
|
+
doc(firestore, 'users', user.uid, 'notifications', token),
|
270
|
+
{
|
271
|
+
token,
|
272
|
+
created: serverTimestamp(),
|
273
|
+
active: true
|
274
|
+
},
|
275
|
+
{ merge: true }
|
276
|
+
);
|
277
|
+
}
|
278
|
+
} catch (error) {
|
279
|
+
console.error('Save subscription error:', error);
|
280
|
+
// Don't throw - this is not critical for the subscription process
|
281
|
+
}
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
export default Notifications;
|
@@ -0,0 +1,166 @@
|
|
1
|
+
class SentryModule {
|
2
|
+
constructor(manager) {
|
3
|
+
this.manager = manager;
|
4
|
+
this.initialized = false;
|
5
|
+
this.Sentry = null;
|
6
|
+
this.config = null;
|
7
|
+
}
|
8
|
+
|
9
|
+
/**
|
10
|
+
* Initialize Sentry error tracking
|
11
|
+
* @param {Object} config - Sentry configuration object
|
12
|
+
* @returns {Promise} Resolves when initialization is complete
|
13
|
+
*/
|
14
|
+
init(config = {}) {
|
15
|
+
return new Promise((resolve, reject) => {
|
16
|
+
// Dynamically import Sentry to reduce initial bundle size
|
17
|
+
import('@sentry/browser')
|
18
|
+
.then((SentryModule) => {
|
19
|
+
// Store reference and expose globally
|
20
|
+
this.Sentry = SentryModule;
|
21
|
+
window.Sentry = SentryModule;
|
22
|
+
|
23
|
+
// Build configuration with our defaults
|
24
|
+
this.config = this._buildConfig(config);
|
25
|
+
|
26
|
+
// Initialize Sentry
|
27
|
+
this.Sentry.init(this.config);
|
28
|
+
this.initialized = true;
|
29
|
+
|
30
|
+
resolve({ initialized: true });
|
31
|
+
})
|
32
|
+
.catch((error) => {
|
33
|
+
console.error('[Sentry] Failed to initialize:', error);
|
34
|
+
reject(error);
|
35
|
+
});
|
36
|
+
});
|
37
|
+
}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Build Sentry configuration with defaults and integrations
|
41
|
+
* @private
|
42
|
+
*/
|
43
|
+
_buildConfig(userConfig) {
|
44
|
+
const config = { ...userConfig };
|
45
|
+
const manager = this.manager;
|
46
|
+
|
47
|
+
// Set release version and environment
|
48
|
+
config.release = `${manager.config.brand.id}@${manager.config.buildTime}`;
|
49
|
+
config.environment = manager.config.environment || 'production';
|
50
|
+
config.integrations = config.integrations || [];
|
51
|
+
|
52
|
+
// Add browser tracing integration if not already present
|
53
|
+
if (!config.integrations.some(i => i.name === 'BrowserTracing')) {
|
54
|
+
config.integrations.push(this.Sentry.browserTracingIntegration());
|
55
|
+
}
|
56
|
+
|
57
|
+
// Add replay integration if sample rates are configured
|
58
|
+
const hasReplays = (config.replaysSessionSampleRate > 0) ||
|
59
|
+
(config.replaysOnErrorSampleRate > 0);
|
60
|
+
|
61
|
+
if (hasReplays && !config.integrations.some(i => i.name === 'Replay')) {
|
62
|
+
config.integrations.push(this.Sentry.replayIntegration({
|
63
|
+
maskAllText: false,
|
64
|
+
blockAllMedia: false,
|
65
|
+
}));
|
66
|
+
}
|
67
|
+
|
68
|
+
// Configure beforeSend to enrich events with user data and session info
|
69
|
+
config.beforeSend = (event, hint) => {
|
70
|
+
const startTime = this.manager.config.page.startTime || Date.now();
|
71
|
+
const hoursSinceStart = (Date.now() - startTime) / (1000 * 3600);
|
72
|
+
const storage = this.manager.storage();
|
73
|
+
|
74
|
+
// Add custom tags
|
75
|
+
event.tags = {
|
76
|
+
...event.tags,
|
77
|
+
'process.type': 'browser',
|
78
|
+
'usage.session.hours': hoursSinceStart.toFixed(2),
|
79
|
+
};
|
80
|
+
|
81
|
+
// Add user info from storage
|
82
|
+
event.user = {
|
83
|
+
...event.user,
|
84
|
+
email: storage.get('user.auth.email', ''),
|
85
|
+
uid: storage.get('user.auth.uid', ''),
|
86
|
+
};
|
87
|
+
|
88
|
+
// Log error to console for debugging
|
89
|
+
console.error('[Sentry] Caught error:', {
|
90
|
+
message: event.message || (event.exception?.values?.[0]?.value) || 'Unknown error',
|
91
|
+
level: event.level,
|
92
|
+
tags: event.tags,
|
93
|
+
user: event.user,
|
94
|
+
hint
|
95
|
+
});
|
96
|
+
|
97
|
+
// Block sending in development mode
|
98
|
+
if (this.manager.isDevelopment()) {
|
99
|
+
console.log('[Sentry] Development mode - not sending to Sentry');
|
100
|
+
return null;
|
101
|
+
}
|
102
|
+
|
103
|
+
return event;
|
104
|
+
};
|
105
|
+
|
106
|
+
return config;
|
107
|
+
}
|
108
|
+
|
109
|
+
/**
|
110
|
+
* Capture an exception and send to Sentry
|
111
|
+
* Safe to call even if Sentry is not initialized
|
112
|
+
* @param {Error} error - The error to capture
|
113
|
+
* @param {Object} captureContext - Additional context for the error
|
114
|
+
* @returns {string|null} Event ID if successful, null otherwise
|
115
|
+
*/
|
116
|
+
captureException(error, captureContext) {
|
117
|
+
// Log the error
|
118
|
+
console.error('[Sentry] Capturing exception:', error);
|
119
|
+
|
120
|
+
// Safe to call - won't throw if not initialized
|
121
|
+
if (!this.initialized) {
|
122
|
+
console.log('[Sentry] Not initialized, skipping capture');
|
123
|
+
return null;
|
124
|
+
}
|
125
|
+
|
126
|
+
// Call Sentry to capture the exception
|
127
|
+
try {
|
128
|
+
return this.Sentry.captureException(error, captureContext);
|
129
|
+
} catch (captureError) {
|
130
|
+
console.error('[Sentry] Failed to capture exception:', captureError);
|
131
|
+
return null;
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
// /**
|
136
|
+
// * Capture a message and send to Sentry
|
137
|
+
// * Safe to call even if Sentry is not initialized
|
138
|
+
// * @param {string} message - The message to capture
|
139
|
+
// * @param {string} level - Severity level (debug, info, warning, error, fatal)
|
140
|
+
// * @param {Object} captureContext - Additional context for the message
|
141
|
+
// * @returns {string|null} Event ID if successful, null otherwise
|
142
|
+
// */
|
143
|
+
// captureMessage(message, level = 'info', captureContext) {
|
144
|
+
// if (!message) {
|
145
|
+
// console.warn('[Sentry] captureMessage called with no message');
|
146
|
+
// return null;
|
147
|
+
// }
|
148
|
+
|
149
|
+
// console.log(`[Sentry] Capturing message (${level}):`, message);
|
150
|
+
|
151
|
+
// // Safe to call - won't throw if not initialized
|
152
|
+
// if (!this.initialized || !this.Sentry) {
|
153
|
+
// console.log('[Sentry] Not initialized, skipping capture');
|
154
|
+
// return null;
|
155
|
+
// }
|
156
|
+
|
157
|
+
// try {
|
158
|
+
// return this.Sentry.captureMessage(message, level, captureContext);
|
159
|
+
// } catch (captureError) {
|
160
|
+
// console.error('[Sentry] Failed to capture message:', captureError);
|
161
|
+
// return null;
|
162
|
+
// }
|
163
|
+
// }
|
164
|
+
}
|
165
|
+
|
166
|
+
export default SentryModule;
|