qdadm 0.29.0 → 0.31.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.
Files changed (49) hide show
  1. package/package.json +11 -2
  2. package/src/components/forms/FormPage.vue +1 -1
  3. package/src/components/index.js +5 -3
  4. package/src/components/layout/AppLayout.vue +13 -1
  5. package/src/components/layout/Zone.vue +40 -23
  6. package/src/composables/index.js +2 -1
  7. package/src/composables/useAuth.js +43 -4
  8. package/src/composables/useCurrentEntity.js +44 -0
  9. package/src/composables/useFormPageBuilder.js +3 -3
  10. package/src/composables/useNavContext.js +24 -8
  11. package/src/composables/useSSEBridge.js +118 -0
  12. package/src/debug/AuthCollector.js +254 -0
  13. package/src/debug/Collector.js +235 -0
  14. package/src/debug/DebugBridge.js +163 -0
  15. package/src/debug/DebugModule.js +215 -0
  16. package/src/debug/EntitiesCollector.js +376 -0
  17. package/src/debug/ErrorCollector.js +66 -0
  18. package/src/debug/LocalStorageAdapter.js +150 -0
  19. package/src/debug/SignalCollector.js +87 -0
  20. package/src/debug/ToastCollector.js +82 -0
  21. package/src/debug/ZonesCollector.js +300 -0
  22. package/src/debug/components/DebugBar.vue +1232 -0
  23. package/src/debug/components/ObjectTree.vue +194 -0
  24. package/src/debug/components/index.js +8 -0
  25. package/src/debug/components/panels/AuthPanel.vue +103 -0
  26. package/src/debug/components/panels/EntitiesPanel.vue +616 -0
  27. package/src/debug/components/panels/EntriesPanel.vue +188 -0
  28. package/src/debug/components/panels/ToastsPanel.vue +112 -0
  29. package/src/debug/components/panels/ZonesPanel.vue +232 -0
  30. package/src/debug/components/panels/index.js +8 -0
  31. package/src/debug/index.js +31 -0
  32. package/src/editors/index.js +12 -0
  33. package/src/entity/EntityManager.js +142 -20
  34. package/src/entity/storage/MockApiStorage.js +17 -1
  35. package/src/entity/storage/index.js +9 -2
  36. package/src/gen/index.js +3 -0
  37. package/src/index.js +7 -0
  38. package/src/kernel/Kernel.js +661 -48
  39. package/src/kernel/KernelContext.js +385 -0
  40. package/src/kernel/Module.js +111 -0
  41. package/src/kernel/ModuleLoader.js +573 -0
  42. package/src/kernel/SSEBridge.js +354 -0
  43. package/src/kernel/SignalBus.js +10 -7
  44. package/src/kernel/index.js +19 -0
  45. package/src/toast/ToastBridgeModule.js +70 -0
  46. package/src/toast/ToastListener.vue +47 -0
  47. package/src/toast/index.js +15 -0
  48. package/src/toast/useSignalToast.js +113 -0
  49. package/src/composables/useSSE.js +0 -212
@@ -0,0 +1,215 @@
1
+ /**
2
+ * DebugModule - Module System v2 class for debug tools integration
3
+ *
4
+ * Integrates the debug infrastructure (DebugBridge, collectors, DebugBar)
5
+ * into a qdadm application via the Module System.
6
+ *
7
+ * Features:
8
+ * - Creates and configures DebugBridge with default collectors
9
+ * - Registers ErrorCollector and SignalCollector automatically
10
+ * - Adds DebugBar component to 'app:debug' zone
11
+ * - Provides debug bridge via Vue's provide/inject
12
+ *
13
+ * @example
14
+ * import { createKernel, DebugModule } from 'qdadm'
15
+ *
16
+ * const kernel = createKernel({ debug: true })
17
+ * kernel.use(new DebugModule({ enabled: true }))
18
+ * await kernel.boot()
19
+ */
20
+
21
+ import { h, inject, defineComponent } from 'vue'
22
+ import { Module } from '../kernel/Module.js'
23
+ import { createDebugBridge } from './DebugBridge.js'
24
+ import { ErrorCollector } from './ErrorCollector.js'
25
+ import { SignalCollector } from './SignalCollector.js'
26
+ import { ToastCollector } from './ToastCollector.js'
27
+ import { ZonesCollector } from './ZonesCollector.js'
28
+ import { AuthCollector } from './AuthCollector.js'
29
+ import { EntitiesCollector } from './EntitiesCollector.js'
30
+ import DebugBar from './components/DebugBar.vue'
31
+
32
+ /**
33
+ * Symbol for debug bridge injection key
34
+ */
35
+ export const DEBUG_BRIDGE_KEY = Symbol('debugBridge')
36
+
37
+ /**
38
+ * Debug zone name for the DebugBar component
39
+ * Prefixed with _ to hide from ZonesCollector (internal zone)
40
+ */
41
+ export const DEBUG_ZONE = '_app:debug'
42
+
43
+ /**
44
+ * Global DebugBar wrapper component
45
+ * Auto-injects the debug bridge - use in App.vue for app-wide debug bar
46
+ *
47
+ * @example
48
+ * <!-- In App.vue template -->
49
+ * <router-view />
50
+ * <QdadmDebugBar />
51
+ */
52
+ export const QdadmDebugBar = defineComponent({
53
+ name: 'QdadmDebugBar',
54
+ setup() {
55
+ const bridge = inject(DEBUG_BRIDGE_KEY, null)
56
+ return () => bridge ? h(DebugBar, { bridge }) : null
57
+ }
58
+ })
59
+
60
+ /**
61
+ * DebugModule - Integrates debug tools into the application
62
+ */
63
+ export class DebugModule extends Module {
64
+ /**
65
+ * Module identifier
66
+ * @type {string}
67
+ */
68
+ static name = 'debug'
69
+
70
+ /**
71
+ * No dependencies - debug module should work standalone
72
+ * @type {string[]}
73
+ */
74
+ static requires = []
75
+
76
+ /**
77
+ * Very low priority - runs last after all modules are connected
78
+ * This ensures all signals and routes are registered before debug tools start
79
+ * @type {number}
80
+ */
81
+ static priority = 1000
82
+
83
+ /**
84
+ * Create a new DebugModule
85
+ *
86
+ * @param {object} [options={}] - Module options
87
+ * @param {boolean} [options.enabled=false] - Initial enabled state for collectors
88
+ * @param {number} [options.maxEntries=100] - Max entries per collector
89
+ * @param {boolean} [options.errorCollector=true] - Include ErrorCollector
90
+ * @param {boolean} [options.signalCollector=true] - Include SignalCollector
91
+ * @param {boolean} [options.toastCollector=true] - Include ToastCollector
92
+ * @param {boolean} [options.zonesCollector=true] - Include ZonesCollector
93
+ * @param {boolean} [options.authCollector=true] - Include AuthCollector
94
+ * @param {boolean} [options.entitiesCollector=true] - Include EntitiesCollector
95
+ */
96
+ constructor(options = {}) {
97
+ super(options)
98
+ this._bridge = null
99
+ this._blockId = 'debug-bar'
100
+ }
101
+
102
+ /**
103
+ * Check if module should be enabled
104
+ *
105
+ * Debug module is enabled when:
106
+ * - In development mode (ctx.isDev)
107
+ * - OR debug option is explicitly true (ctx.debug)
108
+ *
109
+ * @param {import('../kernel/KernelContext.js').KernelContext} ctx
110
+ * @returns {boolean}
111
+ */
112
+ enabled(ctx) {
113
+ return ctx.isDev || ctx.debug
114
+ }
115
+
116
+ /**
117
+ * Connect module to kernel
118
+ *
119
+ * Creates DebugBridge, registers collectors, and sets up DebugBar zone.
120
+ *
121
+ * @param {import('../kernel/KernelContext.js').KernelContext} ctx
122
+ */
123
+ async connect(ctx) {
124
+ this.ctx = ctx
125
+
126
+ // Create debug bridge with options
127
+ this._bridge = createDebugBridge({
128
+ enabled: this.options.enabled ?? false
129
+ })
130
+
131
+ // Register default collectors
132
+ const collectorOptions = { maxEntries: this.options.maxEntries ?? 100 }
133
+
134
+ if (this.options.errorCollector !== false) {
135
+ this._bridge.addCollector(new ErrorCollector(collectorOptions))
136
+ }
137
+
138
+ if (this.options.signalCollector !== false) {
139
+ this._bridge.addCollector(new SignalCollector(collectorOptions))
140
+ }
141
+
142
+ if (this.options.toastCollector !== false) {
143
+ this._bridge.addCollector(new ToastCollector(collectorOptions))
144
+ }
145
+
146
+ if (this.options.zonesCollector !== false) {
147
+ this._bridge.addCollector(new ZonesCollector(collectorOptions))
148
+ }
149
+
150
+ if (this.options.authCollector !== false) {
151
+ this._bridge.addCollector(new AuthCollector(collectorOptions))
152
+ }
153
+
154
+ if (this.options.entitiesCollector !== false) {
155
+ this._bridge.addCollector(new EntitiesCollector(collectorOptions))
156
+ }
157
+
158
+ // Install collectors with context
159
+ this._bridge.install(ctx)
160
+
161
+ // Define the debug zone (for backwards compatibility, but don't render in it)
162
+ // When using debugBar shorthand in Kernel, the root wrapper handles rendering.
163
+ // The zone is still defined so apps can use <QdadmZone name="app:debug" />
164
+ // if they're not using the debugBar shorthand.
165
+ ctx.zone(DEBUG_ZONE)
166
+
167
+ // Only register zone block if NOT using Kernel's root wrapper approach
168
+ // This prevents double-rendering when debugBar shorthand is used.
169
+ // Note: When debugBar shorthand is used, Kernel wraps root with QdadmDebugBar.
170
+ // If we also register in zone, layouts with <Zone name="app:debug" /> would render twice.
171
+ if (!this.options._kernelManaged) {
172
+ ctx.block(DEBUG_ZONE, {
173
+ id: this._blockId,
174
+ component: DebugBar,
175
+ props: {
176
+ bridge: this._bridge
177
+ },
178
+ weight: 100
179
+ })
180
+ }
181
+
182
+ // Provide debug bridge for injection
183
+ ctx.provide(DEBUG_BRIDGE_KEY, this._bridge)
184
+
185
+ // Register global component for use in App.vue (outside authenticated routes)
186
+ ctx.component('QdadmDebugBar', QdadmDebugBar)
187
+ }
188
+
189
+ /**
190
+ * Disconnect module from kernel
191
+ *
192
+ * Cleans up debug bridge and removes DebugBar from zone.
193
+ */
194
+ async disconnect() {
195
+ // Uninstall bridge and all collectors
196
+ if (this._bridge) {
197
+ this._bridge.uninstall()
198
+ this._bridge = null
199
+ }
200
+
201
+ // Call parent to clean up signal listeners
202
+ await super.disconnect()
203
+ }
204
+
205
+ /**
206
+ * Get the debug bridge instance
207
+ *
208
+ * @returns {import('./DebugBridge.js').DebugBridge|null}
209
+ */
210
+ getBridge() {
211
+ return this._bridge
212
+ }
213
+ }
214
+
215
+ export default DebugModule
@@ -0,0 +1,376 @@
1
+ /**
2
+ * EntitiesCollector - Debug collector for Entity Managers
3
+ *
4
+ * This collector displays registered entity managers and their state:
5
+ * - All registered managers from Orchestrator
6
+ * - Storage type and capabilities
7
+ * - Cache status (enabled, valid, items, threshold)
8
+ * - Permissions (readOnly, canCreate, canUpdate, canDelete)
9
+ * - Relations (parent, children, parents)
10
+ *
11
+ * Shows current state rather than historical events.
12
+ *
13
+ * @example
14
+ * const collector = new EntitiesCollector()
15
+ * collector.install(ctx)
16
+ * collector.getEntries() // [{ name: 'books', storage: 'ApiStorage', ... }, ...]
17
+ */
18
+
19
+ import { Collector } from './Collector.js'
20
+
21
+ /**
22
+ * Collector for Entity Manager state visualization
23
+ */
24
+ export class EntitiesCollector extends Collector {
25
+ /**
26
+ * Collector name identifier
27
+ * @type {string}
28
+ */
29
+ static name = 'entities'
30
+
31
+ /**
32
+ * This collector shows state, not events
33
+ * @type {boolean}
34
+ */
35
+ static records = false
36
+
37
+ constructor(options = {}) {
38
+ super(options)
39
+ this._ctx = null
40
+ this._signalCleanups = []
41
+ this._lastUpdate = 0
42
+ // Activity tracking: store previous stats to detect changes
43
+ this._previousStats = new Map() // entityName -> stats snapshot
44
+ this._activeEntities = new Set() // entities with unseen activity
45
+ }
46
+
47
+ /**
48
+ * Internal install - store context reference and subscribe to signals
49
+ * The orchestrator is accessed lazily since it may not exist at install time.
50
+ * @param {object} ctx - Context object
51
+ * @protected
52
+ */
53
+ _doInstall(ctx) {
54
+ this._ctx = ctx
55
+
56
+ // Subscribe to entity signals for reactive updates
57
+ // Uses deferred access since signals may not be ready at install time
58
+ this._setupSignals()
59
+ }
60
+
61
+ /**
62
+ * Setup signal listeners (deferred until signals available)
63
+ * @private
64
+ */
65
+ _setupSignals() {
66
+ const signals = this._ctx?.signals
67
+ if (!signals) {
68
+ // Retry on next tick if signals not ready
69
+ setTimeout(() => this._setupSignals(), 100)
70
+ return
71
+ }
72
+
73
+ // Listen to all entity lifecycle events
74
+ const entityCleanup = signals.on('entity:*', () => {
75
+ this._lastUpdate = Date.now()
76
+ this.notifyChange()
77
+ })
78
+ this._signalCleanups.push(entityCleanup)
79
+
80
+ // Listen to cache invalidation
81
+ const cacheCleanup = signals.on('cache:entity:invalidated', () => {
82
+ this._lastUpdate = Date.now()
83
+ this.notifyChange()
84
+ })
85
+ this._signalCleanups.push(cacheCleanup)
86
+ }
87
+
88
+ /**
89
+ * Internal uninstall - cleanup signal subscriptions
90
+ * @protected
91
+ */
92
+ _doUninstall() {
93
+ // Cleanup signal listeners
94
+ for (const cleanup of this._signalCleanups) {
95
+ if (typeof cleanup === 'function') cleanup()
96
+ }
97
+ this._signalCleanups = []
98
+ this._ctx = null
99
+ }
100
+
101
+ /**
102
+ * Get orchestrator lazily (may not exist at install time)
103
+ * @returns {object|null}
104
+ * @private
105
+ */
106
+ get _orchestrator() {
107
+ return this._ctx?.orchestrator ?? null
108
+ }
109
+
110
+ /**
111
+ * Get badge - show count of entities with unseen activity
112
+ * @returns {number}
113
+ */
114
+ getBadge() {
115
+ if (!this._orchestrator) return 0
116
+ // Check for new activity before returning badge count
117
+ this._detectActivity()
118
+ return this._activeEntities.size
119
+ }
120
+
121
+ /**
122
+ * Detect activity by comparing current stats with previous snapshot
123
+ * @private
124
+ */
125
+ _detectActivity() {
126
+ if (!this._orchestrator) return
127
+
128
+ for (const name of this._orchestrator.getRegisteredNames()) {
129
+ try {
130
+ const manager = this._orchestrator.get(name)
131
+ const currentStats = manager.getStats?.()
132
+ if (!currentStats) continue
133
+
134
+ const prevStats = this._previousStats.get(name)
135
+ if (!prevStats) {
136
+ // First time seeing this entity, store snapshot
137
+ this._previousStats.set(name, { ...currentStats })
138
+ continue
139
+ }
140
+
141
+ // Check if any stat changed
142
+ const hasChanged =
143
+ currentStats.list !== prevStats.list ||
144
+ currentStats.get !== prevStats.get ||
145
+ currentStats.create !== prevStats.create ||
146
+ currentStats.update !== prevStats.update ||
147
+ currentStats.delete !== prevStats.delete ||
148
+ currentStats.cacheHits !== prevStats.cacheHits ||
149
+ currentStats.cacheMisses !== prevStats.cacheMisses
150
+
151
+ if (hasChanged) {
152
+ this._activeEntities.add(name)
153
+ // Update snapshot for next comparison
154
+ this._previousStats.set(name, { ...currentStats })
155
+ }
156
+ } catch (e) {
157
+ // Skip failed managers
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Get total count - number of registered entities
164
+ * @returns {number}
165
+ */
166
+ getTotalCount() {
167
+ if (!this._orchestrator) return 0
168
+ return this._orchestrator.getRegisteredNames().length
169
+ }
170
+
171
+ /**
172
+ * Get all entity information for display
173
+ * @returns {Array<object>} Entity info array
174
+ */
175
+ getEntries() {
176
+ if (!this._orchestrator) {
177
+ return [{ type: 'status', message: 'No orchestrator configured' }]
178
+ }
179
+
180
+ const names = this._orchestrator.getRegisteredNames()
181
+ if (names.length === 0) {
182
+ return [{ type: 'status', message: 'No entities registered' }]
183
+ }
184
+
185
+ const entries = []
186
+
187
+ for (const name of names.sort()) {
188
+ try {
189
+ const manager = this._orchestrator.get(name)
190
+ entries.push(this._buildEntityInfo(name, manager))
191
+ } catch (e) {
192
+ entries.push({
193
+ name,
194
+ error: e.message
195
+ })
196
+ }
197
+ }
198
+
199
+ return entries
200
+ }
201
+
202
+ /**
203
+ * Build entity info object from manager
204
+ * @param {string} name - Entity name
205
+ * @param {EntityManager} manager - Manager instance
206
+ * @returns {object} Entity info
207
+ * @private
208
+ */
209
+ _buildEntityInfo(name, manager) {
210
+ const cache = manager.getCacheInfo?.() || {}
211
+ const storage = manager.storage
212
+
213
+ return {
214
+ name,
215
+ hasActivity: this._activeEntities.has(name),
216
+ label: manager.label,
217
+ labelPlural: manager.labelPlural,
218
+ routePrefix: manager.routePrefix,
219
+ idField: manager.idField,
220
+
221
+ // Storage info
222
+ storage: {
223
+ type: storage?.constructor?.name || 'None',
224
+ endpoint: storage?.endpoint || storage?._endpoint || null,
225
+ capabilities: storage?.constructor?.capabilities || {}
226
+ },
227
+
228
+ // Cache info
229
+ cache: {
230
+ enabled: cache.enabled ?? false,
231
+ valid: cache.valid ?? false,
232
+ itemCount: cache.itemCount ?? 0,
233
+ total: cache.total ?? 0,
234
+ threshold: cache.threshold ?? 0,
235
+ overflow: cache.overflow ?? false,
236
+ loadedAt: cache.loadedAt ? new Date(cache.loadedAt).toLocaleTimeString() : null,
237
+ // Include cached items for inspection (limited to first 50)
238
+ items: this._getCacheItems(manager, 50)
239
+ },
240
+
241
+ // Permissions
242
+ permissions: {
243
+ readOnly: manager.readOnly,
244
+ canCreate: manager.canCreate?.() ?? true,
245
+ canUpdate: manager.canUpdate?.() ?? true,
246
+ canDelete: manager.canDelete?.() ?? true,
247
+ canList: manager.canList?.() ?? true
248
+ },
249
+
250
+ // Warmup
251
+ warmup: {
252
+ enabled: manager.warmupEnabled ?? false
253
+ },
254
+
255
+ // Operation stats
256
+ stats: manager.getStats?.() || {
257
+ list: 0,
258
+ get: 0,
259
+ create: 0,
260
+ update: 0,
261
+ delete: 0,
262
+ cacheHits: 0,
263
+ cacheMisses: 0,
264
+ maxItemsSeen: 0,
265
+ maxTotal: 0
266
+ },
267
+
268
+ // Fields
269
+ fields: {
270
+ count: Object.keys(manager.fields || {}).length,
271
+ names: Object.keys(manager.fields || {}).slice(0, 10), // First 10
272
+ required: manager.getRequiredFields?.() || []
273
+ },
274
+
275
+ // Relations
276
+ relations: {
277
+ parent: manager._parent ? {
278
+ entity: manager._parent.entity,
279
+ foreignKey: manager._parent.foreignKey
280
+ } : null,
281
+ parents: Object.entries(manager._parents || {}).map(([key, config]) => ({
282
+ key,
283
+ entity: config.entity,
284
+ foreignKey: config.foreignKey
285
+ })),
286
+ children: Object.entries(manager._children || {}).map(([key, config]) => ({
287
+ key,
288
+ entity: config.entity,
289
+ endpoint: config.endpoint || null
290
+ }))
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Get cached items from a manager
297
+ * @param {EntityManager} manager - Manager instance
298
+ * @param {number} limit - Max items to return
299
+ * @returns {Array} Cached items (limited)
300
+ * @private
301
+ */
302
+ _getCacheItems(manager, limit = 50) {
303
+ try {
304
+ // Try different cache access methods
305
+ if (manager._cache && Array.isArray(manager._cache)) {
306
+ return manager._cache.slice(0, limit)
307
+ }
308
+ if (manager._cacheMap && manager._cacheMap instanceof Map) {
309
+ return Array.from(manager._cacheMap.values()).slice(0, limit)
310
+ }
311
+ if (manager.cache && Array.isArray(manager.cache)) {
312
+ return manager.cache.slice(0, limit)
313
+ }
314
+ // Try getAll if available and cache is valid
315
+ if (manager.getCacheInfo?.()?.valid && manager._cachedItems) {
316
+ return manager._cachedItems.slice(0, limit)
317
+ }
318
+ return []
319
+ } catch (e) {
320
+ return []
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Force refresh a specific entity's cache
326
+ * @param {string} entityName - Entity name
327
+ * @param {boolean} [reload=false] - If true, reload cache after invalidation
328
+ * @returns {Promise<boolean>} True if cache refreshed
329
+ */
330
+ async refreshCache(entityName, reload = false) {
331
+ if (!this._orchestrator) return false
332
+ try {
333
+ const manager = this._orchestrator.get(entityName)
334
+ manager.invalidateCache()
335
+ if (reload) {
336
+ await manager.ensureCache()
337
+ }
338
+ this.notifyChange()
339
+ return true
340
+ } catch (e) {
341
+ console.error('[EntitiesCollector] Failed to refresh cache:', e)
342
+ return false
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Invalidate a specific entity's cache
348
+ * @param {string} entityName - Entity name
349
+ */
350
+ invalidateCache(entityName) {
351
+ if (!this._orchestrator) return
352
+ try {
353
+ const manager = this._orchestrator.get(entityName)
354
+ manager.invalidateCache()
355
+ } catch (e) {
356
+ console.error('[EntitiesCollector] Failed to invalidate cache:', e)
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Mark all entities as seen (clear activity state)
362
+ * Call this when the panel is viewed
363
+ * Note: Does not call notifyChange() to avoid re-render loop
364
+ */
365
+ markSeen() {
366
+ this._activeEntities.clear()
367
+ }
368
+
369
+ /**
370
+ * Mark a specific entity as seen
371
+ * @param {string} entityName - Entity name
372
+ */
373
+ markEntitySeen(entityName) {
374
+ this._activeEntities.delete(entityName)
375
+ }
376
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * ErrorCollector - Captures JavaScript errors and unhandled promise rejections
3
+ *
4
+ * This collector listens to global window error events and unhandled promise
5
+ * rejections, recording them for display in the debug panel.
6
+ *
7
+ * @example
8
+ * const collector = new ErrorCollector()
9
+ * collector.install(ctx)
10
+ * // Errors are now automatically recorded
11
+ * // Later...
12
+ * collector.uninstall()
13
+ */
14
+
15
+ import { Collector } from './Collector.js'
16
+
17
+ /**
18
+ * Collector for JavaScript errors and unhandled promise rejections
19
+ */
20
+ export class ErrorCollector extends Collector {
21
+ /**
22
+ * Collector name identifier
23
+ * @type {string}
24
+ */
25
+ static name = 'errors'
26
+
27
+ /**
28
+ * Internal install - subscribe to error and unhandledrejection events
29
+ * @param {object} ctx - Context object (not used for error collection)
30
+ * @protected
31
+ */
32
+ _doInstall(ctx) {
33
+ this._handler = (event) => {
34
+ this.record({
35
+ message: event.message,
36
+ filename: event.filename,
37
+ lineno: event.lineno,
38
+ colno: event.colno,
39
+ error: event.error?.stack
40
+ })
41
+ }
42
+ this._rejectionHandler = (event) => {
43
+ this.record({
44
+ message: 'Unhandled Promise Rejection',
45
+ reason: String(event.reason)
46
+ })
47
+ }
48
+ window.addEventListener('error', this._handler)
49
+ window.addEventListener('unhandledrejection', this._rejectionHandler)
50
+ }
51
+
52
+ /**
53
+ * Internal uninstall - remove event listeners
54
+ * @protected
55
+ */
56
+ _doUninstall() {
57
+ if (this._handler) {
58
+ window.removeEventListener('error', this._handler)
59
+ this._handler = null
60
+ }
61
+ if (this._rejectionHandler) {
62
+ window.removeEventListener('unhandledrejection', this._rejectionHandler)
63
+ this._rejectionHandler = null
64
+ }
65
+ }
66
+ }