@pie-players/pie-players-shared 0.2.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 (233) hide show
  1. package/dist/config/profile.d.ts +15 -0
  2. package/dist/config/profile.d.ts.map +1 -0
  3. package/dist/config/profile.js +27 -0
  4. package/dist/config/profile.js.map +1 -0
  5. package/dist/i18n/index.d.ts +13 -0
  6. package/dist/i18n/index.d.ts.map +1 -0
  7. package/dist/i18n/index.js +12 -0
  8. package/dist/i18n/index.js.map +1 -0
  9. package/dist/i18n/loader.d.ts +36 -0
  10. package/dist/i18n/loader.d.ts.map +1 -0
  11. package/dist/i18n/loader.js +133 -0
  12. package/dist/i18n/loader.js.map +1 -0
  13. package/dist/i18n/scripts/check-coverage.d.ts +16 -0
  14. package/dist/i18n/scripts/check-coverage.d.ts.map +1 -0
  15. package/dist/i18n/scripts/check-coverage.js +262 -0
  16. package/dist/i18n/scripts/check-coverage.js.map +1 -0
  17. package/dist/i18n/scripts/scan-hardcoded.d.ts +16 -0
  18. package/dist/i18n/scripts/scan-hardcoded.d.ts.map +1 -0
  19. package/dist/i18n/scripts/scan-hardcoded.js +266 -0
  20. package/dist/i18n/scripts/scan-hardcoded.js.map +1 -0
  21. package/dist/i18n/simple-i18n.d.ts +69 -0
  22. package/dist/i18n/simple-i18n.d.ts.map +1 -0
  23. package/dist/i18n/simple-i18n.js +199 -0
  24. package/dist/i18n/simple-i18n.js.map +1 -0
  25. package/dist/i18n/translations/ar/common.json +36 -0
  26. package/dist/i18n/translations/ar/toolkit.json +48 -0
  27. package/dist/i18n/translations/ar/tools.json +109 -0
  28. package/dist/i18n/translations/en/common.json +36 -0
  29. package/dist/i18n/translations/en/toolkit.json +48 -0
  30. package/dist/i18n/translations/en/tools.json +109 -0
  31. package/dist/i18n/translations/es/common.json +36 -0
  32. package/dist/i18n/translations/es/toolkit.json +48 -0
  33. package/dist/i18n/translations/es/tools.json +109 -0
  34. package/dist/i18n/translations/zh/common.json +36 -0
  35. package/dist/i18n/translations/zh/toolkit.json +48 -0
  36. package/dist/i18n/translations/zh/tools.json +109 -0
  37. package/dist/i18n/types.d.ts +58 -0
  38. package/dist/i18n/types.d.ts.map +1 -0
  39. package/dist/i18n/types.js +8 -0
  40. package/dist/i18n/types.js.map +1 -0
  41. package/dist/i18n/use-i18n-standalone.svelte.d.ts +87 -0
  42. package/dist/i18n/use-i18n-standalone.svelte.d.ts.map +1 -0
  43. package/dist/i18n/use-i18n-standalone.svelte.js +151 -0
  44. package/dist/i18n/use-i18n-standalone.svelte.js.map +1 -0
  45. package/dist/i18n/use-i18n.svelte.d.ts +67 -0
  46. package/dist/i18n/use-i18n.svelte.d.ts.map +1 -0
  47. package/dist/i18n/use-i18n.svelte.js +144 -0
  48. package/dist/i18n/use-i18n.svelte.js.map +1 -0
  49. package/dist/index.d.ts +11 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +11 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/instrumentation/index.d.ts +53 -0
  54. package/dist/instrumentation/index.d.ts.map +1 -0
  55. package/dist/instrumentation/index.js +53 -0
  56. package/dist/instrumentation/index.js.map +1 -0
  57. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +197 -0
  58. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts.map +1 -0
  59. package/dist/instrumentation/providers/BaseInstrumentationProvider.js +267 -0
  60. package/dist/instrumentation/providers/BaseInstrumentationProvider.js.map +1 -0
  61. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts +106 -0
  62. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts.map +1 -0
  63. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js +182 -0
  64. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js.map +1 -0
  65. package/dist/instrumentation/providers/DataDogInstrumentationProvider.d.ts +170 -0
  66. package/dist/instrumentation/providers/DataDogInstrumentationProvider.d.ts.map +1 -0
  67. package/dist/instrumentation/providers/DataDogInstrumentationProvider.js +183 -0
  68. package/dist/instrumentation/providers/DataDogInstrumentationProvider.js.map +1 -0
  69. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +86 -0
  70. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts.map +1 -0
  71. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js +135 -0
  72. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js.map +1 -0
  73. package/dist/instrumentation/providers/index.d.ts +12 -0
  74. package/dist/instrumentation/providers/index.d.ts.map +1 -0
  75. package/dist/instrumentation/providers/index.js +12 -0
  76. package/dist/instrumentation/providers/index.js.map +1 -0
  77. package/dist/instrumentation/types.d.ts +348 -0
  78. package/dist/instrumentation/types.d.ts.map +1 -0
  79. package/dist/instrumentation/types.js +9 -0
  80. package/dist/instrumentation/types.js.map +1 -0
  81. package/dist/loader-config.d.ts +76 -0
  82. package/dist/loader-config.d.ts.map +1 -0
  83. package/dist/loader-config.js +12 -0
  84. package/dist/loader-config.js.map +1 -0
  85. package/dist/loaders/ElementLoader.d.ts +72 -0
  86. package/dist/loaders/ElementLoader.d.ts.map +1 -0
  87. package/dist/loaders/ElementLoader.js +52 -0
  88. package/dist/loaders/ElementLoader.js.map +1 -0
  89. package/dist/loaders/EsmElementLoader.d.ts +67 -0
  90. package/dist/loaders/EsmElementLoader.d.ts.map +1 -0
  91. package/dist/loaders/EsmElementLoader.js +71 -0
  92. package/dist/loaders/EsmElementLoader.js.map +1 -0
  93. package/dist/loaders/IifeElementLoader.d.ts +61 -0
  94. package/dist/loaders/IifeElementLoader.d.ts.map +1 -0
  95. package/dist/loaders/IifeElementLoader.js +63 -0
  96. package/dist/loaders/IifeElementLoader.js.map +1 -0
  97. package/dist/loaders/index.d.ts +28 -0
  98. package/dist/loaders/index.d.ts.map +1 -0
  99. package/dist/loaders/index.js +25 -0
  100. package/dist/loaders/index.js.map +1 -0
  101. package/dist/object/index.d.ts +12 -0
  102. package/dist/object/index.d.ts.map +1 -0
  103. package/dist/object/index.js +40 -0
  104. package/dist/object/index.js.map +1 -0
  105. package/dist/pie/asset-handler.d.ts +64 -0
  106. package/dist/pie/asset-handler.d.ts.map +1 -0
  107. package/dist/pie/asset-handler.js +238 -0
  108. package/dist/pie/asset-handler.js.map +1 -0
  109. package/dist/pie/component-context.d.ts +22 -0
  110. package/dist/pie/component-context.d.ts.map +1 -0
  111. package/dist/pie/component-context.js +30 -0
  112. package/dist/pie/component-context.js.map +1 -0
  113. package/dist/pie/config.d.ts +39 -0
  114. package/dist/pie/config.d.ts.map +1 -0
  115. package/dist/pie/config.js +174 -0
  116. package/dist/pie/config.js.map +1 -0
  117. package/dist/pie/configure-initialization.d.ts +35 -0
  118. package/dist/pie/configure-initialization.d.ts.map +1 -0
  119. package/dist/pie/configure-initialization.js +141 -0
  120. package/dist/pie/configure-initialization.js.map +1 -0
  121. package/dist/pie/esm-loader.d.ts +93 -0
  122. package/dist/pie/esm-loader.d.ts.map +1 -0
  123. package/dist/pie/esm-loader.js +308 -0
  124. package/dist/pie/esm-loader.js.map +1 -0
  125. package/dist/pie/iife-loader.d.ts +76 -0
  126. package/dist/pie/iife-loader.d.ts.map +1 -0
  127. package/dist/pie/iife-loader.js +303 -0
  128. package/dist/pie/iife-loader.js.map +1 -0
  129. package/dist/pie/index.d.ts +31 -0
  130. package/dist/pie/index.d.ts.map +1 -0
  131. package/dist/pie/index.js +34 -0
  132. package/dist/pie/index.js.map +1 -0
  133. package/dist/pie/initialization.d.ts +40 -0
  134. package/dist/pie/initialization.d.ts.map +1 -0
  135. package/dist/pie/initialization.js +349 -0
  136. package/dist/pie/initialization.js.map +1 -0
  137. package/dist/pie/logger.d.ts +64 -0
  138. package/dist/pie/logger.d.ts.map +1 -0
  139. package/dist/pie/logger.js +45 -0
  140. package/dist/pie/logger.js.map +1 -0
  141. package/dist/pie/math-rendering.d.ts +69 -0
  142. package/dist/pie/math-rendering.d.ts.map +1 -0
  143. package/dist/pie/math-rendering.js +98 -0
  144. package/dist/pie/math-rendering.js.map +1 -0
  145. package/dist/pie/overrides.d.ts +43 -0
  146. package/dist/pie/overrides.d.ts.map +1 -0
  147. package/dist/pie/overrides.js +146 -0
  148. package/dist/pie/overrides.js.map +1 -0
  149. package/dist/pie/player-initializer.d.ts +55 -0
  150. package/dist/pie/player-initializer.d.ts.map +1 -0
  151. package/dist/pie/player-initializer.js +123 -0
  152. package/dist/pie/player-initializer.js.map +1 -0
  153. package/dist/pie/registry.d.ts +11 -0
  154. package/dist/pie/registry.d.ts.map +1 -0
  155. package/dist/pie/registry.js +21 -0
  156. package/dist/pie/registry.js.map +1 -0
  157. package/dist/pie/resource-monitor.d.ts +208 -0
  158. package/dist/pie/resource-monitor.d.ts.map +1 -0
  159. package/dist/pie/resource-monitor.js +969 -0
  160. package/dist/pie/resource-monitor.js.map +1 -0
  161. package/dist/pie/scoring.d.ts +17 -0
  162. package/dist/pie/scoring.d.ts.map +1 -0
  163. package/dist/pie/scoring.js +84 -0
  164. package/dist/pie/scoring.js.map +1 -0
  165. package/dist/pie/types.d.ts +136 -0
  166. package/dist/pie/types.d.ts.map +1 -0
  167. package/dist/pie/types.js +52 -0
  168. package/dist/pie/types.js.map +1 -0
  169. package/dist/pie/updates.d.ts +20 -0
  170. package/dist/pie/updates.d.ts.map +1 -0
  171. package/dist/pie/updates.js +175 -0
  172. package/dist/pie/updates.js.map +1 -0
  173. package/dist/pie/use-resource-monitor.svelte.d.ts +56 -0
  174. package/dist/pie/use-resource-monitor.svelte.d.ts.map +1 -0
  175. package/dist/pie/use-resource-monitor.svelte.js +117 -0
  176. package/dist/pie/use-resource-monitor.svelte.js.map +1 -0
  177. package/dist/pie/utils.d.ts +44 -0
  178. package/dist/pie/utils.d.ts.map +1 -0
  179. package/dist/pie/utils.js +74 -0
  180. package/dist/pie/utils.js.map +1 -0
  181. package/dist/types/custom-elements.d.ts +183 -0
  182. package/dist/types/custom-elements.d.ts.map +1 -0
  183. package/dist/types/custom-elements.js +8 -0
  184. package/dist/types/custom-elements.js.map +1 -0
  185. package/dist/types/index.d.ts +761 -0
  186. package/dist/types/index.d.ts.map +1 -0
  187. package/dist/types/index.js +120 -0
  188. package/dist/types/index.js.map +1 -0
  189. package/dist/types/search.d.ts +105 -0
  190. package/dist/types/search.d.ts.map +1 -0
  191. package/dist/types/search.js +12 -0
  192. package/dist/types/search.js.map +1 -0
  193. package/dist/types/transform.d.ts +48 -0
  194. package/dist/types/transform.d.ts.map +1 -0
  195. package/dist/types/transform.js +21 -0
  196. package/dist/types/transform.js.map +1 -0
  197. package/dist/ui/focus-trap.d.ts +10 -0
  198. package/dist/ui/focus-trap.d.ts.map +1 -0
  199. package/dist/ui/focus-trap.js +30 -0
  200. package/dist/ui/focus-trap.js.map +1 -0
  201. package/dist/ui/safe-storage.d.ts +3 -0
  202. package/dist/ui/safe-storage.d.ts.map +1 -0
  203. package/dist/ui/safe-storage.js +21 -0
  204. package/dist/ui/safe-storage.js.map +1 -0
  205. package/package.json +118 -0
  206. package/src/components/PieItemPlayer.svelte +604 -0
  207. package/src/components/PiePreviewLayout.svelte +144 -0
  208. package/src/components/PiePreviewToggle.svelte +110 -0
  209. package/src/components/PieSpinner.svelte +85 -0
  210. package/src/components/ToolSettingsButton.svelte +31 -0
  211. package/src/components/ToolSettingsPanel.svelte +90 -0
  212. package/src/components/index.ts +6 -0
  213. package/src/i18n/README.md +223 -0
  214. package/src/i18n/index.ts +26 -0
  215. package/src/i18n/loader.ts +156 -0
  216. package/src/i18n/scripts/check-coverage.ts +345 -0
  217. package/src/i18n/scripts/scan-hardcoded.ts +342 -0
  218. package/src/i18n/simple-i18n.ts +236 -0
  219. package/src/i18n/translations/ar/common.json +36 -0
  220. package/src/i18n/translations/ar/toolkit.json +48 -0
  221. package/src/i18n/translations/ar/tools.json +109 -0
  222. package/src/i18n/translations/en/common.json +36 -0
  223. package/src/i18n/translations/en/toolkit.json +48 -0
  224. package/src/i18n/translations/en/tools.json +109 -0
  225. package/src/i18n/translations/es/common.json +36 -0
  226. package/src/i18n/translations/es/toolkit.json +48 -0
  227. package/src/i18n/translations/es/tools.json +109 -0
  228. package/src/i18n/translations/zh/common.json +36 -0
  229. package/src/i18n/translations/zh/toolkit.json +48 -0
  230. package/src/i18n/translations/zh/tools.json +109 -0
  231. package/src/i18n/types.ts +66 -0
  232. package/src/i18n/use-i18n-standalone.svelte.ts +184 -0
  233. package/src/i18n/use-i18n.svelte.ts +163 -0
@@ -0,0 +1,969 @@
1
+ /**
2
+ * Resource Monitor for PIE Element Assets
3
+ *
4
+ * Tracks and monitors loading of resources (audio, video, images) embedded
5
+ * in PIE element content without requiring changes to PIE elements.
6
+ *
7
+ * Features:
8
+ * - Tracks resource load timing with PerformanceObserver
9
+ * - Detects and retries failed resource loads
10
+ * - Sends instrumentation to New Relic
11
+ * - Works with all resource types (audio, video, img, link)
12
+ */
13
+ import { NewRelicInstrumentationProvider } from "../instrumentation/providers/NewRelicInstrumentationProvider";
14
+ import { getCurrentComponentContext } from "./component-context";
15
+ import { createPieLogger } from "./logger";
16
+ const DEFAULT_CONFIG = {
17
+ trackPageActions: false,
18
+ instrumentationProvider: undefined,
19
+ maxRetries: 3,
20
+ initialRetryDelay: 500,
21
+ maxRetryDelay: 5000,
22
+ debug: false,
23
+ };
24
+ // Constants
25
+ const MAX_URL_LENGTH = 80;
26
+ const URL_TRUNCATE_LENGTH = 77;
27
+ /**
28
+ * Tracks resource loads and provides retry capability
29
+ */
30
+ export class ResourceMonitor {
31
+ config;
32
+ logger;
33
+ observer = null;
34
+ mutationObserver = null;
35
+ errorHandler = null;
36
+ retryAttempts = new Map();
37
+ container = null;
38
+ isBrowser;
39
+ containerResources = new Set(); // Track resources within our container
40
+ provider;
41
+ constructor(config = {}) {
42
+ this.config = {
43
+ trackPageActions: config.trackPageActions ?? DEFAULT_CONFIG.trackPageActions,
44
+ instrumentationProvider: config.instrumentationProvider ??
45
+ DEFAULT_CONFIG.instrumentationProvider,
46
+ maxRetries: config.maxRetries ?? DEFAULT_CONFIG.maxRetries,
47
+ initialRetryDelay: config.initialRetryDelay ?? DEFAULT_CONFIG.initialRetryDelay,
48
+ maxRetryDelay: config.maxRetryDelay ?? DEFAULT_CONFIG.maxRetryDelay,
49
+ debug: config.debug ?? DEFAULT_CONFIG.debug,
50
+ };
51
+ // Always use a provider - default to NewRelic if not specified
52
+ this.provider =
53
+ this.config.instrumentationProvider ??
54
+ new NewRelicInstrumentationProvider();
55
+ // Initialize the provider (async, but don't block constructor)
56
+ this.provider.initialize().catch((err) => {
57
+ if (this.config.debug) {
58
+ console.warn("[ResourceMonitor] Failed to initialize instrumentation provider:", err);
59
+ }
60
+ });
61
+ this.logger = createPieLogger("resource-monitor", () => this.isDebugEnabled());
62
+ this.isBrowser =
63
+ typeof window !== "undefined" && typeof document !== "undefined";
64
+ }
65
+ /**
66
+ * Check if debug logging is enabled (dynamically checks window.PIE_DEBUG)
67
+ */
68
+ isDebugEnabled() {
69
+ return (this.config.debug ||
70
+ (typeof window !== "undefined" && window.PIE_DEBUG === true));
71
+ }
72
+ /**
73
+ * Truncate URL for display in logs
74
+ */
75
+ truncateUrl(url) {
76
+ return url.length > MAX_URL_LENGTH
77
+ ? "..." + url.slice(-URL_TRUNCATE_LENGTH)
78
+ : url;
79
+ }
80
+ /**
81
+ * Strip retry parameters from URL to get the original URL
82
+ */
83
+ getOriginalUrl(url) {
84
+ try {
85
+ const urlObj = new URL(url);
86
+ urlObj.searchParams.delete("retry");
87
+ urlObj.searchParams.delete("t");
88
+ return urlObj.toString();
89
+ }
90
+ catch {
91
+ // If URL parsing fails, return as-is
92
+ return url;
93
+ }
94
+ }
95
+ /**
96
+ * Track event with instrumentation provider if enabled
97
+ */
98
+ trackInstrumentationEvent(eventName, attributes) {
99
+ if (!this.config.trackPageActions || !this.provider.isReady()) {
100
+ return;
101
+ }
102
+ this.provider.trackEvent(eventName, {
103
+ ...attributes,
104
+ component: "resource-monitor",
105
+ timestamp: new Date().toISOString(),
106
+ });
107
+ }
108
+ /**
109
+ * Track error with instrumentation provider if enabled
110
+ */
111
+ trackInstrumentationError(error, attributes) {
112
+ if (!this.config.trackPageActions || !this.provider.isReady()) {
113
+ return;
114
+ }
115
+ this.provider.trackError(error, {
116
+ ...attributes,
117
+ component: "resource-monitor",
118
+ errorType: attributes.errorType || "ResourceError",
119
+ });
120
+ }
121
+ /**
122
+ * Start monitoring resources in the given container
123
+ */
124
+ start(container) {
125
+ if (!this.isBrowser) {
126
+ this.logger.debug("Not in browser environment, skipping resource monitoring");
127
+ return;
128
+ }
129
+ this.container = container;
130
+ this.setupMutationObserver();
131
+ this.scanContainerResources(); // Initial scan of existing resources
132
+ this.setupPerformanceObserver();
133
+ this.setupErrorHandler();
134
+ this.logger.info("✅ Resource monitoring started");
135
+ }
136
+ /**
137
+ * Stop monitoring and clean up
138
+ */
139
+ stop() {
140
+ if (this.observer) {
141
+ this.observer.disconnect();
142
+ this.observer = null;
143
+ }
144
+ if (this.mutationObserver) {
145
+ this.mutationObserver.disconnect();
146
+ this.mutationObserver = null;
147
+ }
148
+ if (this.errorHandler && this.container) {
149
+ this.container.removeEventListener("error", this.errorHandler, true);
150
+ this.errorHandler = null;
151
+ }
152
+ this.retryAttempts.clear();
153
+ this.containerResources.clear();
154
+ this.container = null;
155
+ this.logger.info("Resource monitoring stopped");
156
+ }
157
+ /**
158
+ * Set up MutationObserver to track DOM changes within container
159
+ * This allows us to know which resources belong to our container
160
+ */
161
+ setupMutationObserver() {
162
+ if (!this.isBrowser ||
163
+ typeof MutationObserver === "undefined" ||
164
+ !this.container) {
165
+ this.logger.debug("MutationObserver not available or no container");
166
+ return;
167
+ }
168
+ try {
169
+ this.mutationObserver = new MutationObserver((mutations) => {
170
+ for (const mutation of mutations) {
171
+ // Check added nodes
172
+ mutation.addedNodes.forEach((node) => {
173
+ if (node instanceof HTMLElement) {
174
+ this.scanElementForResources(node);
175
+ }
176
+ });
177
+ // Check attribute changes (src, href changes)
178
+ if (mutation.type === "attributes" &&
179
+ mutation.target instanceof HTMLElement) {
180
+ const target = mutation.target;
181
+ if (this.isResourceElement(target)) {
182
+ const src = this.getResourceSrc(target);
183
+ if (src) {
184
+ this.containerResources.add(src);
185
+ if (this.isDebugEnabled()) {
186
+ this.logger.debug(`📌 Tracked resource attribute change: ${src}`);
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
192
+ });
193
+ // Observe the container for changes
194
+ this.mutationObserver.observe(this.container, {
195
+ childList: true,
196
+ subtree: true,
197
+ attributes: true,
198
+ attributeFilter: ["src", "href"],
199
+ });
200
+ this.logger.debug("MutationObserver set up successfully");
201
+ }
202
+ catch (error) {
203
+ this.logger.warn("Failed to set up MutationObserver:", error);
204
+ }
205
+ }
206
+ /**
207
+ * Scan an element and its descendants for resources
208
+ */
209
+ scanElementForResources(element) {
210
+ // Check the element itself
211
+ if (this.isResourceElement(element)) {
212
+ const src = this.getResourceSrc(element);
213
+ if (src) {
214
+ this.containerResources.add(src);
215
+ if (this.isDebugEnabled()) {
216
+ this.logger.debug(`📌 Tracked new resource in container: ${src}`);
217
+ }
218
+ }
219
+ }
220
+ // Check descendants
221
+ const resourceSelectors = [
222
+ "img",
223
+ "audio",
224
+ "video",
225
+ 'link[rel="stylesheet"]',
226
+ "source",
227
+ ];
228
+ resourceSelectors.forEach((selector) => {
229
+ element.querySelectorAll(selector).forEach((el) => {
230
+ if (this.isResourceElement(el)) {
231
+ const src = this.getResourceSrc(el);
232
+ if (src) {
233
+ this.containerResources.add(src);
234
+ }
235
+ }
236
+ });
237
+ });
238
+ }
239
+ /**
240
+ * Initial scan of container for existing resources
241
+ */
242
+ scanContainerResources() {
243
+ if (!this.container) {
244
+ return;
245
+ }
246
+ this.scanElementForResources(this.container);
247
+ if (this.isDebugEnabled()) {
248
+ this.logger.debug(`📊 Initial container scan found ${this.containerResources.size} resources`);
249
+ if (this.containerResources.size > 0) {
250
+ this.containerResources.forEach((url) => {
251
+ this.logger.debug(` - ${this.truncateUrl(url)}`);
252
+ });
253
+ }
254
+ }
255
+ }
256
+ /**
257
+ * Set up PerformanceObserver to track resource loading timing
258
+ */
259
+ setupPerformanceObserver() {
260
+ if (!this.isBrowser || typeof PerformanceObserver === "undefined") {
261
+ this.logger.debug("PerformanceObserver not available");
262
+ return;
263
+ }
264
+ try {
265
+ this.observer = new PerformanceObserver((list) => {
266
+ for (const entry of list.getEntries()) {
267
+ if (entry.entryType === "resource") {
268
+ this.handleResourceTiming(entry);
269
+ }
270
+ }
271
+ });
272
+ // Use 'type' (singular) instead of 'entryTypes' (plural) to support buffered option
273
+ // The buffered flag only works with the newer 'type' parameter
274
+ this.observer.observe({
275
+ type: "resource",
276
+ buffered: true, // Capture resources loaded before observer started
277
+ });
278
+ this.logger.debug("PerformanceObserver set up successfully");
279
+ }
280
+ catch (error) {
281
+ this.logger.warn("Failed to set up PerformanceObserver:", error);
282
+ }
283
+ }
284
+ /**
285
+ * Handle resource timing entry
286
+ */
287
+ handleResourceTiming(entry) {
288
+ // Only track media and image resources that are relevant to PIE content
289
+ const isRelevant = this.isRelevantResource(entry);
290
+ if (!isRelevant) {
291
+ if (this.isDebugEnabled() &&
292
+ (entry.initiatorType === "img" ||
293
+ entry.initiatorType === "audio" ||
294
+ entry.initiatorType === "video")) {
295
+ this.logger.debug(`⏭️ Skipping non-container resource: ${this.truncateUrl(entry.name)}`);
296
+ }
297
+ return;
298
+ }
299
+ const duration = entry.duration;
300
+ const size = entry.transferSize;
301
+ const url = entry.name;
302
+ // Detect actual failures: responseEnd === 0 means the request didn't complete
303
+ // Note: transferSize === 0 is NOT a failure indicator (can be cache, CORS, or small resources)
304
+ // We primarily rely on the error event handler for detecting failures
305
+ const failed = entry.responseEnd === 0 && entry.duration > 0;
306
+ // Check if this was a retry that succeeded
307
+ const wasRetried = this.retryAttempts.has(url);
308
+ const retryCount = this.retryAttempts.get(url) || 0;
309
+ // Enhanced debug logging with detailed timing breakdown
310
+ if (this.isDebugEnabled()) {
311
+ const shortUrl = this.truncateUrl(url);
312
+ const sizeKB = (size / 1024).toFixed(2);
313
+ const status = failed ? "❌ FAILED" : "✅ SUCCESS";
314
+ // Detailed timing breakdown
315
+ const timingDetails = {
316
+ total: `${duration.toFixed(2)}ms`,
317
+ dns: entry.domainLookupEnd > 0
318
+ ? `${(entry.domainLookupEnd - entry.domainLookupStart).toFixed(2)}ms`
319
+ : "n/a",
320
+ tcp: entry.connectEnd > 0
321
+ ? `${(entry.connectEnd - entry.connectStart).toFixed(2)}ms`
322
+ : "n/a",
323
+ request: entry.responseStart > 0
324
+ ? `${(entry.responseStart - entry.requestStart).toFixed(2)}ms`
325
+ : "n/a",
326
+ response: entry.responseEnd > 0
327
+ ? `${(entry.responseEnd - entry.responseStart).toFixed(2)}ms`
328
+ : "n/a",
329
+ size: size > 0 ? `${sizeKB} KB` : "0 KB",
330
+ type: entry.initiatorType,
331
+ protocol: entry.nextHopProtocol || "unknown",
332
+ };
333
+ // Add retry context if this was retried
334
+ const retryContext = wasRetried && !failed
335
+ ? `\n 🔄 Retry Success: Succeeded after ${retryCount} ${retryCount === 1 ? "retry" : "retries"}`
336
+ : "";
337
+ this.logger.info(`📊 PIE Resource Load ${status}\n` +
338
+ ` URL: ${shortUrl}\n` +
339
+ ` Type: ${timingDetails.type} | Protocol: ${timingDetails.protocol}\n` +
340
+ ` ⏱️ Total Time: ${timingDetails.total}\n` +
341
+ ` └─ DNS Lookup: ${timingDetails.dns}\n` +
342
+ ` └─ TCP Connect: ${timingDetails.tcp}\n` +
343
+ ` └─ Request Time: ${timingDetails.request}\n` +
344
+ ` └─ Response Time: ${timingDetails.response}\n` +
345
+ ` 📦 Transfer Size: ${timingDetails.size}${retryContext}`);
346
+ }
347
+ else {
348
+ // Simple logging when debug is off but tracking is on
349
+ this.logger.debug(`Resource loaded: ${entry.name}`, {
350
+ duration: `${duration.toFixed(2)}ms`,
351
+ size: `${size} bytes`,
352
+ type: entry.initiatorType,
353
+ failed,
354
+ wasRetried,
355
+ retryCount,
356
+ });
357
+ }
358
+ // Handle successful loads
359
+ if (!failed) {
360
+ this.handleSuccessfulLoad(url, entry, duration, size, retryCount, wasRetried);
361
+ }
362
+ // Track with instrumentation provider
363
+ this.trackInstrumentationEvent("pie-resource-load", {
364
+ url: entry.name,
365
+ duration: Math.round(duration),
366
+ size,
367
+ type: entry.initiatorType,
368
+ failed,
369
+ wasRetried,
370
+ retryCount,
371
+ });
372
+ // Track failed loads
373
+ if (failed) {
374
+ this.handleFailedLoad(url, entry, duration, retryCount, wasRetried);
375
+ }
376
+ }
377
+ /**
378
+ * Handle successful resource load
379
+ */
380
+ handleSuccessfulLoad(url, entry, duration, size, retryCount, wasRetried) {
381
+ // Dispatch general success event (for any successful load)
382
+ this.dispatchEvent("pie-resource-load-success", {
383
+ url,
384
+ resourceType: entry.initiatorType,
385
+ duration,
386
+ size,
387
+ retryCount,
388
+ maxRetries: this.config.maxRetries,
389
+ });
390
+ // Log successful retry specifically
391
+ if (wasRetried) {
392
+ const shortUrl = this.truncateUrl(url);
393
+ this.logger.info(`✅ PIE Resource Retry Succeeded!\n` +
394
+ ` URL: ${shortUrl}\n` +
395
+ ` Retry Attempt: ${retryCount}\n` +
396
+ ` Load Time: ${duration.toFixed(2)}ms\n` +
397
+ ` Result: Resource now available to user`);
398
+ // Dispatch retry success event (in addition to general success)
399
+ this.dispatchEvent("pie-resource-retry-success", {
400
+ url,
401
+ resourceType: entry.initiatorType,
402
+ duration,
403
+ size,
404
+ retryCount,
405
+ maxRetries: this.config.maxRetries,
406
+ });
407
+ // Clear retry tracking since it succeeded
408
+ this.retryAttempts.delete(url);
409
+ }
410
+ }
411
+ /**
412
+ * Handle failed resource load
413
+ */
414
+ handleFailedLoad(url, entry, duration, retryCount, wasRetried) {
415
+ const shortUrl = this.truncateUrl(url);
416
+ if (wasRetried) {
417
+ // This is a retry that also failed - use warn since we'll retry again
418
+ this.logger.warn(`⚠️ PIE Resource Retry Failed\n` +
419
+ ` URL: ${shortUrl}\n` +
420
+ ` Retry Attempt: ${retryCount}\n` +
421
+ ` Remaining Attempts: ${this.config.maxRetries - retryCount}\n` +
422
+ ` Status: Will ${retryCount >= this.config.maxRetries ? "give up" : "retry again"}`);
423
+ // Dispatch retry failure event
424
+ this.dispatchEvent("pie-resource-retry-failed", {
425
+ url,
426
+ resourceType: entry.initiatorType,
427
+ duration,
428
+ retryCount,
429
+ maxRetries: this.config.maxRetries,
430
+ error: "Resource load failed after retry",
431
+ });
432
+ }
433
+ else {
434
+ // Initial failure - use warn since we'll retry
435
+ this.logger.warn(`⚠️ PIE Resource Initial Load Failed\n` +
436
+ ` URL: ${shortUrl}\n` +
437
+ ` Status: Will attempt ${this.config.maxRetries} ${this.config.maxRetries === 1 ? "retry" : "retries"}`);
438
+ // Dispatch initial failure event
439
+ this.dispatchEvent("pie-resource-load-failed", {
440
+ url,
441
+ resourceType: entry.initiatorType,
442
+ duration,
443
+ retryCount: 0,
444
+ maxRetries: this.config.maxRetries,
445
+ error: "Initial resource load failed",
446
+ });
447
+ }
448
+ // Track error with instrumentation provider
449
+ this.trackInstrumentationError(new Error(`Resource load failed: ${entry.name}`), {
450
+ resourceUrl: entry.name,
451
+ resourceType: entry.initiatorType,
452
+ duration: Math.round(duration),
453
+ wasRetried,
454
+ retryCount,
455
+ });
456
+ }
457
+ /**
458
+ * Check if resource is relevant to our container
459
+ * Uses container-scoped tracking via MutationObserver
460
+ * Also retroactively checks if resource belongs to container if not yet tracked
461
+ */
462
+ isRelevantResource(entry) {
463
+ const url = entry.name;
464
+ // Only track resources that we know are in our container
465
+ let isInContainer = this.containerResources.has(url);
466
+ if (!isInContainer) {
467
+ // Also check if it's a relative URL that might match
468
+ // (PerformanceResourceTiming gives absolute URLs)
469
+ for (const containerUrl of this.containerResources) {
470
+ if (url.endsWith(containerUrl) || containerUrl.endsWith(url)) {
471
+ return true;
472
+ }
473
+ }
474
+ // Retroactively check if this resource belongs to our container
475
+ // This handles the case where resources load before MutationObserver scans
476
+ // PerformanceObserver can capture resources that loaded before it started (buffered: true)
477
+ // but if they loaded before MutationObserver scanned, they won't be in containerResources
478
+ if (this.container) {
479
+ // Simple check: if resource is a media/image type and we have a container,
480
+ // check if any element in container has this src/href
481
+ // This is a fallback for resources that loaded very quickly before scan completed
482
+ const isInContainer = this.isResourceInContainer(url, entry.initiatorType);
483
+ if (isInContainer) {
484
+ this.containerResources.add(url);
485
+ if (this.isDebugEnabled()) {
486
+ this.logger.debug(`📌 Retroactively tracked resource: ${this.truncateUrl(url)}`);
487
+ }
488
+ return true;
489
+ }
490
+ }
491
+ }
492
+ return isInContainer;
493
+ }
494
+ /**
495
+ * Check if a resource URL actually belongs to our container by checking DOM elements
496
+ * This is a fallback for resources that loaded before MutationObserver scanned
497
+ */
498
+ isResourceInContainer(url, initiatorType) {
499
+ if (!this.container) {
500
+ return false;
501
+ }
502
+ try {
503
+ // Check if any element in the container has this URL as src/href
504
+ const urlObj = new URL(url);
505
+ const urlPath = urlObj.pathname + urlObj.search;
506
+ // For images, audio, video - check src attributes
507
+ if (initiatorType === "img" ||
508
+ initiatorType === "audio" ||
509
+ initiatorType === "video") {
510
+ const elements = this.container.querySelectorAll(`${initiatorType}[src]`);
511
+ for (const el of elements) {
512
+ const resourceEl = el;
513
+ if (resourceEl.src &&
514
+ (resourceEl.src === url || resourceEl.src.endsWith(urlPath))) {
515
+ return true;
516
+ }
517
+ }
518
+ }
519
+ // For link elements (stylesheets) - check href
520
+ if (initiatorType === "link") {
521
+ const links = this.container.querySelectorAll("link[href]");
522
+ for (const link of links) {
523
+ const linkEl = link;
524
+ if (linkEl.href &&
525
+ (linkEl.href === url || linkEl.href.endsWith(urlPath))) {
526
+ return true;
527
+ }
528
+ }
529
+ }
530
+ // For source elements (inside audio/video) - check src
531
+ if (initiatorType === "source") {
532
+ const sources = this.container.querySelectorAll("source[src]");
533
+ for (const source of sources) {
534
+ const sourceEl = source;
535
+ if (sourceEl.src &&
536
+ (sourceEl.src === url || sourceEl.src.endsWith(urlPath))) {
537
+ return true;
538
+ }
539
+ }
540
+ }
541
+ }
542
+ catch (error) {
543
+ // If URL parsing fails or querySelector fails, fall back to false
544
+ if (this.isDebugEnabled()) {
545
+ this.logger.debug(`Error checking if resource is in container: ${error}`);
546
+ }
547
+ }
548
+ return false;
549
+ }
550
+ /**
551
+ * Set up error event handler for resource loading failures
552
+ */
553
+ setupErrorHandler() {
554
+ if (!this.container) {
555
+ return;
556
+ }
557
+ // Use capturing phase to catch errors before they bubble
558
+ this.errorHandler = (event) => {
559
+ const target = event.target;
560
+ // Only handle resource elements
561
+ if (!this.isResourceElement(target)) {
562
+ return;
563
+ }
564
+ const tagName = target.tagName.toLowerCase();
565
+ const src = this.getResourceSrc(target);
566
+ if (!src) {
567
+ return;
568
+ }
569
+ // Get the original URL without retry parameters
570
+ const originalSrc = this.getOriginalUrl(src);
571
+ // Check if we have retries remaining
572
+ const currentRetries = this.retryAttempts.get(originalSrc) || 0;
573
+ const remainingRetries = this.config.maxRetries - currentRetries;
574
+ const willRetry = remainingRetries > 0;
575
+ // Enhanced debug logging for errors
576
+ // Use warn if we'll retry, error if we've exhausted retries
577
+ if (this.isDebugEnabled()) {
578
+ const shortUrl = this.truncateUrl(src);
579
+ const logMethod = willRetry
580
+ ? this.logger.warn.bind(this.logger)
581
+ : this.logger.error.bind(this.logger);
582
+ const icon = willRetry ? "⚠️" : "❌";
583
+ logMethod(`${icon} PIE Resource Load Error\n` +
584
+ ` Element: <${tagName}>\n` +
585
+ ` URL: ${shortUrl}\n` +
586
+ ` Current Attempts: ${currentRetries}\n` +
587
+ ` Remaining Retries: ${remainingRetries}/${this.config.maxRetries}\n` +
588
+ ` Action: ${willRetry ? "Will retry with exponential backoff" : "Max retries reached, giving up"}`);
589
+ }
590
+ else {
591
+ const logMethod = willRetry
592
+ ? this.logger.warn.bind(this.logger)
593
+ : this.logger.error.bind(this.logger);
594
+ const icon = willRetry ? "⚠️" : "❌";
595
+ logMethod(`${icon} Resource error: ${tagName} failed to load ${src}`);
596
+ }
597
+ // Track error with instrumentation provider
598
+ this.trackInstrumentationError(new Error(`Resource load error: ${originalSrc}`), {
599
+ resourceType: tagName,
600
+ resourceUrl: originalSrc,
601
+ });
602
+ // Attempt retry with original URL
603
+ this.retryResourceLoad(target, originalSrc);
604
+ };
605
+ this.container.addEventListener("error", this.errorHandler, true);
606
+ this.logger.debug("Error handler attached to container");
607
+ }
608
+ /**
609
+ * Check if element is a resource element
610
+ */
611
+ isResourceElement(element) {
612
+ if (!element || !(element instanceof HTMLElement)) {
613
+ return false;
614
+ }
615
+ const tag = element.tagName.toLowerCase();
616
+ return ["img", "audio", "video", "link", "source"].includes(tag);
617
+ }
618
+ /**
619
+ * Get resource src/href from element
620
+ */
621
+ getResourceSrc(element) {
622
+ if (element instanceof HTMLLinkElement) {
623
+ return element.href;
624
+ }
625
+ // For audio, video, img, and source elements (all have src)
626
+ if ("src" in element && element.src) {
627
+ return element.src;
628
+ }
629
+ return null;
630
+ }
631
+ /**
632
+ * Handle permanent resource failure after all retries exhausted
633
+ */
634
+ handlePermanentFailure(url, resourceType, retryCount) {
635
+ if (this.isDebugEnabled()) {
636
+ const shortUrl = this.truncateUrl(url);
637
+ this.logger.error(`❌ PIE Resource Permanently Failed\n` +
638
+ ` URL: ${shortUrl}\n` +
639
+ ` Total Attempts: ${retryCount + 1} (initial + ${retryCount} retries)\n` +
640
+ ` Status: Giving up after ${this.config.maxRetries} retries\n` +
641
+ ` ⚠️ This resource will not be available to the user`);
642
+ }
643
+ else {
644
+ this.logger.error(`❌ Failed to load resource after ${this.config.maxRetries} retries: ${url}`);
645
+ }
646
+ // Dispatch permanent failure event
647
+ this.dispatchEvent("pie-resource-load-error", {
648
+ url,
649
+ resourceType,
650
+ retryCount,
651
+ maxRetries: this.config.maxRetries,
652
+ error: `Resource permanently failed after ${this.config.maxRetries} retries`,
653
+ });
654
+ // Track final failure with instrumentation provider
655
+ this.trackInstrumentationError(new Error(`Resource permanently failed after ${this.config.maxRetries} retries: ${url}`), {
656
+ resourceUrl: url,
657
+ retryCount,
658
+ resourceType,
659
+ });
660
+ }
661
+ /**
662
+ * Log retry schedule information
663
+ */
664
+ logRetrySchedule(url, retryCount, delay, elementTag) {
665
+ if (this.isDebugEnabled()) {
666
+ const shortUrl = this.truncateUrl(url);
667
+ const nextDelay = Math.min(this.config.initialRetryDelay * Math.pow(2, retryCount + 1), this.config.maxRetryDelay);
668
+ const strategy = elementTag === "img"
669
+ ? "Cache-busting URL"
670
+ : elementTag === "audio" || elementTag === "video"
671
+ ? "element.load()"
672
+ : elementTag === "link"
673
+ ? "Cache-busting URL"
674
+ : "URL update";
675
+ this.logger.info(`🔄 PIE Resource Retry Scheduled\n` +
676
+ ` URL: ${shortUrl}\n` +
677
+ ` Attempt: ${retryCount + 1}/${this.config.maxRetries}\n` +
678
+ ` ⏰ Wait Time: ${delay}ms (exponential backoff)\n` +
679
+ ` Next Retry Delay: ${nextDelay}ms (if this fails)\n` +
680
+ ` Strategy: ${strategy}`);
681
+ }
682
+ else {
683
+ this.logger.info(`🔄 Retrying resource load (attempt ${retryCount + 1}/${this.config.maxRetries}) after ${delay}ms: ${url}`);
684
+ }
685
+ }
686
+ /**
687
+ * Retry loading a failed resource with exponential backoff
688
+ */
689
+ async retryResourceLoad(element, originalSrc) {
690
+ const retryCount = this.retryAttempts.get(originalSrc) || 0;
691
+ if (retryCount >= this.config.maxRetries) {
692
+ this.handlePermanentFailure(originalSrc, element.tagName.toLowerCase(), retryCount);
693
+ return;
694
+ }
695
+ // Calculate backoff delay (exponential)
696
+ const delay = Math.min(this.config.initialRetryDelay * Math.pow(2, retryCount), this.config.maxRetryDelay);
697
+ // Log retry schedule
698
+ this.logRetrySchedule(originalSrc, retryCount, delay, element.tagName.toLowerCase());
699
+ // Track retry attempt with instrumentation provider
700
+ this.trackInstrumentationEvent("pie-resource-retry", {
701
+ url: originalSrc,
702
+ attempt: retryCount + 1,
703
+ delay,
704
+ });
705
+ // Wait before retrying
706
+ await new Promise((resolve) => setTimeout(resolve, delay));
707
+ if (this.isDebugEnabled()) {
708
+ this.logger.debug(`⏱️ Retry wait completed (${delay}ms), attempting reload now...`);
709
+ }
710
+ // Increment retry count
711
+ this.retryAttempts.set(originalSrc, retryCount + 1);
712
+ // Attempt reload based on element type
713
+ try {
714
+ if (element instanceof HTMLAudioElement ||
715
+ element instanceof HTMLVideoElement) {
716
+ // For media elements, use load() method
717
+ element.load();
718
+ if (this.isDebugEnabled()) {
719
+ this.logger.debug(`✓ Triggered load() on <${element.tagName.toLowerCase()}>`);
720
+ }
721
+ }
722
+ else if (element instanceof HTMLImageElement) {
723
+ // For images, force reload by appending cache-busting parameter
724
+ const url = new URL(originalSrc);
725
+ url.searchParams.set("retry", (retryCount + 1).toString());
726
+ url.searchParams.set("t", Date.now().toString());
727
+ element.src = url.toString();
728
+ if (this.isDebugEnabled()) {
729
+ this.logger.debug(`✓ Updated <img> src with cache-busting params: retry=${retryCount + 1}, t=${Date.now()}`);
730
+ }
731
+ }
732
+ else if (element instanceof HTMLLinkElement) {
733
+ // For stylesheets, update href with cache-busting params (avoid clone/replace for Shady DOM compatibility)
734
+ const url = new URL(originalSrc);
735
+ url.searchParams.set("retry", (retryCount + 1).toString());
736
+ url.searchParams.set("t", Date.now().toString());
737
+ element.href = url.toString();
738
+ if (this.isDebugEnabled()) {
739
+ this.logger.debug(`✓ Updated <link> href with cache-busting params: retry=${retryCount + 1}, t=${Date.now()}`);
740
+ }
741
+ }
742
+ }
743
+ catch (error) {
744
+ if (this.isDebugEnabled()) {
745
+ this.logger.error(`❌ Error during retry attempt for ${originalSrc}:`, error);
746
+ }
747
+ else {
748
+ this.logger.error(`Error during retry for ${originalSrc}:`, error);
749
+ }
750
+ }
751
+ }
752
+ /**
753
+ * Dispatch a custom event from the container
754
+ */
755
+ dispatchEvent(eventName, detail) {
756
+ if (!this.container) {
757
+ // Silent return - if container is null, events can't be dispatched
758
+ // This should only happen if ResourceMonitor wasn't properly initialized
759
+ return;
760
+ }
761
+ const event = new CustomEvent(eventName, {
762
+ detail,
763
+ bubbles: true,
764
+ composed: true, // Allow crossing shadow DOM boundaries
765
+ });
766
+ this.container.dispatchEvent(event);
767
+ }
768
+ /**
769
+ * Get current retry statistics
770
+ */
771
+ getStats() {
772
+ const failedResources = [];
773
+ this.retryAttempts.forEach((attempts, url) => {
774
+ failedResources.push({ url, attempts });
775
+ });
776
+ return {
777
+ activeRetries: this.retryAttempts.size,
778
+ failedResources: failedResources.sort((a, b) => b.attempts - a.attempts),
779
+ };
780
+ }
781
+ }
782
+ /**
783
+ * Create and start a resource monitor for a container
784
+ */
785
+ export function createResourceMonitor(container, config = {}) {
786
+ const monitor = new ResourceMonitor(config);
787
+ monitor.start(container);
788
+ return monitor;
789
+ }
790
+ // =============================================================================
791
+ // Global resource request tracking (consolidated from the old font-request-tracker)
792
+ // =============================================================================
793
+ // Track failed resource requests (by URL)
794
+ const failedRequests = new Map();
795
+ function isResourceFile(url) {
796
+ const resourceExtensions = [
797
+ // Fonts
798
+ ".woff",
799
+ ".woff2",
800
+ ".ttf",
801
+ ".otf",
802
+ ".eot",
803
+ // Images
804
+ ".gif",
805
+ ".jpg",
806
+ ".jpeg",
807
+ ".png",
808
+ ".svg",
809
+ ".webp",
810
+ ".ico",
811
+ // Audio/Video
812
+ ".mp3",
813
+ ".mp4",
814
+ ".wav",
815
+ ".ogg",
816
+ ".webm",
817
+ // Other assets
818
+ ".pdf",
819
+ ".css",
820
+ ".js",
821
+ ];
822
+ return resourceExtensions.some((ext) => url.toLowerCase().includes(ext));
823
+ }
824
+ function getResourceType(url) {
825
+ if (url.match(/\.(woff|woff2|ttf|otf|eot)/i))
826
+ return "Font";
827
+ if (url.match(/\.(gif|jpg|jpeg|png|svg|webp|ico)/i))
828
+ return "Image";
829
+ if (url.match(/\.(mp3|mp4|wav|ogg|webm)/i))
830
+ return "Media";
831
+ if (url.match(/\.(css)/i))
832
+ return "Stylesheet";
833
+ if (url.match(/\.(js)/i))
834
+ return "Script";
835
+ return "Resource";
836
+ }
837
+ function logFailedRequest(url, status, context) {
838
+ // Only log 404s and other failures (status 0 for network errors)
839
+ if (status !== 404 && status !== 0) {
840
+ return;
841
+ }
842
+ const key = url;
843
+ if (!failedRequests.has(key)) {
844
+ failedRequests.set(key, []);
845
+ }
846
+ const contexts = failedRequests.get(key);
847
+ contexts.push({
848
+ ...(context || { componentName: "Unknown", timestamp: Date.now() }),
849
+ timestamp: Date.now(),
850
+ });
851
+ const resourceType = getResourceType(url);
852
+ const filename = url.split("/").pop() || url;
853
+ const itemId = context?.itemId || "Unknown";
854
+ const componentName = context?.componentName || "Unknown component";
855
+ const elementType = context?.elementType || "";
856
+ console.warn(`Failed ${resourceType} request`, `\n Resource: ${filename}`, `\n URL: ${url}`, `\n Item ID: ${itemId}`, `\n Mini-Player: ${componentName}`, elementType ? `\n Element Type: ${elementType}` : "", `\n Status: ${status}`, context
857
+ ? ""
858
+ : "\n Note: Component not tracked - may be from item markup or PIE element");
859
+ }
860
+ /**
861
+ * Initialize global resource request tracking (404s for fonts/images/etc.)
862
+ * Call this once when the app loads (client-side only).
863
+ */
864
+ export function initializeResourceRequestTracking() {
865
+ if (typeof window === "undefined")
866
+ return;
867
+ // Track fetch requests
868
+ const originalFetch = window.fetch;
869
+ window.fetch = async function (...args) {
870
+ // Extract URL from first argument (can be string, URL, or Request)
871
+ let url = "";
872
+ if (typeof args[0] === "string") {
873
+ url = args[0];
874
+ }
875
+ else if (args[0] instanceof URL) {
876
+ url = args[0].toString();
877
+ }
878
+ else if (args[0] instanceof Request) {
879
+ url = args[0].url;
880
+ }
881
+ if (isResourceFile(url) || url.startsWith("/")) {
882
+ const context = getCurrentComponentContext();
883
+ try {
884
+ const response = await originalFetch(...args);
885
+ if (response.status === 404) {
886
+ logFailedRequest(url, response.status, context);
887
+ }
888
+ return response;
889
+ }
890
+ catch (error) {
891
+ logFailedRequest(url, 0, context);
892
+ throw error;
893
+ }
894
+ }
895
+ return originalFetch(...args);
896
+ };
897
+ // Track XMLHttpRequest
898
+ const originalXHROpen = XMLHttpRequest.prototype.open;
899
+ XMLHttpRequest.prototype.open = function (method, url, async, username, password) {
900
+ const urlString = typeof url === "string" ? url : url.toString();
901
+ if (isResourceFile(urlString) || urlString.startsWith("/")) {
902
+ const context = getCurrentComponentContext();
903
+ this.addEventListener("load", function () {
904
+ if (this.status === 404) {
905
+ logFailedRequest(urlString, this.status, context);
906
+ }
907
+ });
908
+ this.addEventListener("error", function () {
909
+ logFailedRequest(urlString, 0, context);
910
+ });
911
+ }
912
+ return originalXHROpen.call(this, method, url, async ?? true, username ?? null, password ?? null);
913
+ };
914
+ // Intercept img tag error events (most reliable way to catch failed image loads)
915
+ const errorHandler = (event) => {
916
+ const target = event.target;
917
+ if (target instanceof HTMLImageElement && target.src) {
918
+ const url = target.src;
919
+ if (isResourceFile(url) ||
920
+ url.startsWith("/") ||
921
+ url.startsWith(window.location.origin + "/")) {
922
+ const context = getCurrentComponentContext();
923
+ const relativeUrl = url.startsWith(window.location.origin)
924
+ ? url.replace(window.location.origin, "")
925
+ : url;
926
+ logFailedRequest(relativeUrl, 404, context);
927
+ }
928
+ }
929
+ };
930
+ document.addEventListener("error", errorHandler, true);
931
+ // Track resource requests via PerformanceObserver (fonts, CSS resources, etc.)
932
+ const observer = new PerformanceObserver((list) => {
933
+ for (const entry of list.getEntries()) {
934
+ if (entry.entryType !== "resource")
935
+ continue;
936
+ const resourceEntry = entry;
937
+ const url = resourceEntry.name;
938
+ if (isResourceFile(url) ||
939
+ url.startsWith("/") ||
940
+ url.startsWith(window.location.origin + "/")) {
941
+ const perf = resourceEntry;
942
+ const failed = perf.responseEnd === 0 && perf.duration > 0;
943
+ const mightBe404 = perf.transferSize === 0 &&
944
+ perf.responseEnd > 0 &&
945
+ perf.responseStart === 0;
946
+ if (failed || mightBe404) {
947
+ const context = getCurrentComponentContext();
948
+ const relativeUrl = url.startsWith(window.location.origin)
949
+ ? url.replace(window.location.origin, "")
950
+ : url;
951
+ logFailedRequest(relativeUrl, 404, context);
952
+ }
953
+ }
954
+ }
955
+ });
956
+ try {
957
+ observer.observe({ type: "resource", buffered: true });
958
+ }
959
+ catch (e) {
960
+ console.warn("Failed to set up PerformanceObserver for resource tracking:", e);
961
+ }
962
+ }
963
+ export function getTrackedResourceRequests() {
964
+ return new Map(failedRequests);
965
+ }
966
+ export function clearTrackedResourceRequests() {
967
+ failedRequests.clear();
968
+ }
969
+ //# sourceMappingURL=resource-monitor.js.map