@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.
- package/dist/config/profile.d.ts +15 -0
- package/dist/config/profile.d.ts.map +1 -0
- package/dist/config/profile.js +27 -0
- package/dist/config/profile.js.map +1 -0
- package/dist/i18n/index.d.ts +13 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +12 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/loader.d.ts +36 -0
- package/dist/i18n/loader.d.ts.map +1 -0
- package/dist/i18n/loader.js +133 -0
- package/dist/i18n/loader.js.map +1 -0
- package/dist/i18n/scripts/check-coverage.d.ts +16 -0
- package/dist/i18n/scripts/check-coverage.d.ts.map +1 -0
- package/dist/i18n/scripts/check-coverage.js +262 -0
- package/dist/i18n/scripts/check-coverage.js.map +1 -0
- package/dist/i18n/scripts/scan-hardcoded.d.ts +16 -0
- package/dist/i18n/scripts/scan-hardcoded.d.ts.map +1 -0
- package/dist/i18n/scripts/scan-hardcoded.js +266 -0
- package/dist/i18n/scripts/scan-hardcoded.js.map +1 -0
- package/dist/i18n/simple-i18n.d.ts +69 -0
- package/dist/i18n/simple-i18n.d.ts.map +1 -0
- package/dist/i18n/simple-i18n.js +199 -0
- package/dist/i18n/simple-i18n.js.map +1 -0
- package/dist/i18n/translations/ar/common.json +36 -0
- package/dist/i18n/translations/ar/toolkit.json +48 -0
- package/dist/i18n/translations/ar/tools.json +109 -0
- package/dist/i18n/translations/en/common.json +36 -0
- package/dist/i18n/translations/en/toolkit.json +48 -0
- package/dist/i18n/translations/en/tools.json +109 -0
- package/dist/i18n/translations/es/common.json +36 -0
- package/dist/i18n/translations/es/toolkit.json +48 -0
- package/dist/i18n/translations/es/tools.json +109 -0
- package/dist/i18n/translations/zh/common.json +36 -0
- package/dist/i18n/translations/zh/toolkit.json +48 -0
- package/dist/i18n/translations/zh/tools.json +109 -0
- package/dist/i18n/types.d.ts +58 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +8 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/i18n/use-i18n-standalone.svelte.d.ts +87 -0
- package/dist/i18n/use-i18n-standalone.svelte.d.ts.map +1 -0
- package/dist/i18n/use-i18n-standalone.svelte.js +151 -0
- package/dist/i18n/use-i18n-standalone.svelte.js.map +1 -0
- package/dist/i18n/use-i18n.svelte.d.ts +67 -0
- package/dist/i18n/use-i18n.svelte.d.ts.map +1 -0
- package/dist/i18n/use-i18n.svelte.js +144 -0
- package/dist/i18n/use-i18n.svelte.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation/index.d.ts +53 -0
- package/dist/instrumentation/index.d.ts.map +1 -0
- package/dist/instrumentation/index.js +53 -0
- package/dist/instrumentation/index.js.map +1 -0
- package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +197 -0
- package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts.map +1 -0
- package/dist/instrumentation/providers/BaseInstrumentationProvider.js +267 -0
- package/dist/instrumentation/providers/BaseInstrumentationProvider.js.map +1 -0
- package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts +106 -0
- package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts.map +1 -0
- package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js +182 -0
- package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js.map +1 -0
- package/dist/instrumentation/providers/DataDogInstrumentationProvider.d.ts +170 -0
- package/dist/instrumentation/providers/DataDogInstrumentationProvider.d.ts.map +1 -0
- package/dist/instrumentation/providers/DataDogInstrumentationProvider.js +183 -0
- package/dist/instrumentation/providers/DataDogInstrumentationProvider.js.map +1 -0
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +86 -0
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts.map +1 -0
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js +135 -0
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js.map +1 -0
- package/dist/instrumentation/providers/index.d.ts +12 -0
- package/dist/instrumentation/providers/index.d.ts.map +1 -0
- package/dist/instrumentation/providers/index.js +12 -0
- package/dist/instrumentation/providers/index.js.map +1 -0
- package/dist/instrumentation/types.d.ts +348 -0
- package/dist/instrumentation/types.d.ts.map +1 -0
- package/dist/instrumentation/types.js +9 -0
- package/dist/instrumentation/types.js.map +1 -0
- package/dist/loader-config.d.ts +76 -0
- package/dist/loader-config.d.ts.map +1 -0
- package/dist/loader-config.js +12 -0
- package/dist/loader-config.js.map +1 -0
- package/dist/loaders/ElementLoader.d.ts +72 -0
- package/dist/loaders/ElementLoader.d.ts.map +1 -0
- package/dist/loaders/ElementLoader.js +52 -0
- package/dist/loaders/ElementLoader.js.map +1 -0
- package/dist/loaders/EsmElementLoader.d.ts +67 -0
- package/dist/loaders/EsmElementLoader.d.ts.map +1 -0
- package/dist/loaders/EsmElementLoader.js +71 -0
- package/dist/loaders/EsmElementLoader.js.map +1 -0
- package/dist/loaders/IifeElementLoader.d.ts +61 -0
- package/dist/loaders/IifeElementLoader.d.ts.map +1 -0
- package/dist/loaders/IifeElementLoader.js +63 -0
- package/dist/loaders/IifeElementLoader.js.map +1 -0
- package/dist/loaders/index.d.ts +28 -0
- package/dist/loaders/index.d.ts.map +1 -0
- package/dist/loaders/index.js +25 -0
- package/dist/loaders/index.js.map +1 -0
- package/dist/object/index.d.ts +12 -0
- package/dist/object/index.d.ts.map +1 -0
- package/dist/object/index.js +40 -0
- package/dist/object/index.js.map +1 -0
- package/dist/pie/asset-handler.d.ts +64 -0
- package/dist/pie/asset-handler.d.ts.map +1 -0
- package/dist/pie/asset-handler.js +238 -0
- package/dist/pie/asset-handler.js.map +1 -0
- package/dist/pie/component-context.d.ts +22 -0
- package/dist/pie/component-context.d.ts.map +1 -0
- package/dist/pie/component-context.js +30 -0
- package/dist/pie/component-context.js.map +1 -0
- package/dist/pie/config.d.ts +39 -0
- package/dist/pie/config.d.ts.map +1 -0
- package/dist/pie/config.js +174 -0
- package/dist/pie/config.js.map +1 -0
- package/dist/pie/configure-initialization.d.ts +35 -0
- package/dist/pie/configure-initialization.d.ts.map +1 -0
- package/dist/pie/configure-initialization.js +141 -0
- package/dist/pie/configure-initialization.js.map +1 -0
- package/dist/pie/esm-loader.d.ts +93 -0
- package/dist/pie/esm-loader.d.ts.map +1 -0
- package/dist/pie/esm-loader.js +308 -0
- package/dist/pie/esm-loader.js.map +1 -0
- package/dist/pie/iife-loader.d.ts +76 -0
- package/dist/pie/iife-loader.d.ts.map +1 -0
- package/dist/pie/iife-loader.js +303 -0
- package/dist/pie/iife-loader.js.map +1 -0
- package/dist/pie/index.d.ts +31 -0
- package/dist/pie/index.d.ts.map +1 -0
- package/dist/pie/index.js +34 -0
- package/dist/pie/index.js.map +1 -0
- package/dist/pie/initialization.d.ts +40 -0
- package/dist/pie/initialization.d.ts.map +1 -0
- package/dist/pie/initialization.js +349 -0
- package/dist/pie/initialization.js.map +1 -0
- package/dist/pie/logger.d.ts +64 -0
- package/dist/pie/logger.d.ts.map +1 -0
- package/dist/pie/logger.js +45 -0
- package/dist/pie/logger.js.map +1 -0
- package/dist/pie/math-rendering.d.ts +69 -0
- package/dist/pie/math-rendering.d.ts.map +1 -0
- package/dist/pie/math-rendering.js +98 -0
- package/dist/pie/math-rendering.js.map +1 -0
- package/dist/pie/overrides.d.ts +43 -0
- package/dist/pie/overrides.d.ts.map +1 -0
- package/dist/pie/overrides.js +146 -0
- package/dist/pie/overrides.js.map +1 -0
- package/dist/pie/player-initializer.d.ts +55 -0
- package/dist/pie/player-initializer.d.ts.map +1 -0
- package/dist/pie/player-initializer.js +123 -0
- package/dist/pie/player-initializer.js.map +1 -0
- package/dist/pie/registry.d.ts +11 -0
- package/dist/pie/registry.d.ts.map +1 -0
- package/dist/pie/registry.js +21 -0
- package/dist/pie/registry.js.map +1 -0
- package/dist/pie/resource-monitor.d.ts +208 -0
- package/dist/pie/resource-monitor.d.ts.map +1 -0
- package/dist/pie/resource-monitor.js +969 -0
- package/dist/pie/resource-monitor.js.map +1 -0
- package/dist/pie/scoring.d.ts +17 -0
- package/dist/pie/scoring.d.ts.map +1 -0
- package/dist/pie/scoring.js +84 -0
- package/dist/pie/scoring.js.map +1 -0
- package/dist/pie/types.d.ts +136 -0
- package/dist/pie/types.d.ts.map +1 -0
- package/dist/pie/types.js +52 -0
- package/dist/pie/types.js.map +1 -0
- package/dist/pie/updates.d.ts +20 -0
- package/dist/pie/updates.d.ts.map +1 -0
- package/dist/pie/updates.js +175 -0
- package/dist/pie/updates.js.map +1 -0
- package/dist/pie/use-resource-monitor.svelte.d.ts +56 -0
- package/dist/pie/use-resource-monitor.svelte.d.ts.map +1 -0
- package/dist/pie/use-resource-monitor.svelte.js +117 -0
- package/dist/pie/use-resource-monitor.svelte.js.map +1 -0
- package/dist/pie/utils.d.ts +44 -0
- package/dist/pie/utils.d.ts.map +1 -0
- package/dist/pie/utils.js +74 -0
- package/dist/pie/utils.js.map +1 -0
- package/dist/types/custom-elements.d.ts +183 -0
- package/dist/types/custom-elements.d.ts.map +1 -0
- package/dist/types/custom-elements.js +8 -0
- package/dist/types/custom-elements.js.map +1 -0
- package/dist/types/index.d.ts +761 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +120 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/search.d.ts +105 -0
- package/dist/types/search.d.ts.map +1 -0
- package/dist/types/search.js +12 -0
- package/dist/types/search.js.map +1 -0
- package/dist/types/transform.d.ts +48 -0
- package/dist/types/transform.d.ts.map +1 -0
- package/dist/types/transform.js +21 -0
- package/dist/types/transform.js.map +1 -0
- package/dist/ui/focus-trap.d.ts +10 -0
- package/dist/ui/focus-trap.d.ts.map +1 -0
- package/dist/ui/focus-trap.js +30 -0
- package/dist/ui/focus-trap.js.map +1 -0
- package/dist/ui/safe-storage.d.ts +3 -0
- package/dist/ui/safe-storage.d.ts.map +1 -0
- package/dist/ui/safe-storage.js +21 -0
- package/dist/ui/safe-storage.js.map +1 -0
- package/package.json +118 -0
- package/src/components/PieItemPlayer.svelte +604 -0
- package/src/components/PiePreviewLayout.svelte +144 -0
- package/src/components/PiePreviewToggle.svelte +110 -0
- package/src/components/PieSpinner.svelte +85 -0
- package/src/components/ToolSettingsButton.svelte +31 -0
- package/src/components/ToolSettingsPanel.svelte +90 -0
- package/src/components/index.ts +6 -0
- package/src/i18n/README.md +223 -0
- package/src/i18n/index.ts +26 -0
- package/src/i18n/loader.ts +156 -0
- package/src/i18n/scripts/check-coverage.ts +345 -0
- package/src/i18n/scripts/scan-hardcoded.ts +342 -0
- package/src/i18n/simple-i18n.ts +236 -0
- package/src/i18n/translations/ar/common.json +36 -0
- package/src/i18n/translations/ar/toolkit.json +48 -0
- package/src/i18n/translations/ar/tools.json +109 -0
- package/src/i18n/translations/en/common.json +36 -0
- package/src/i18n/translations/en/toolkit.json +48 -0
- package/src/i18n/translations/en/tools.json +109 -0
- package/src/i18n/translations/es/common.json +36 -0
- package/src/i18n/translations/es/toolkit.json +48 -0
- package/src/i18n/translations/es/tools.json +109 -0
- package/src/i18n/translations/zh/common.json +36 -0
- package/src/i18n/translations/zh/toolkit.json +48 -0
- package/src/i18n/translations/zh/tools.json +109 -0
- package/src/i18n/types.ts +66 -0
- package/src/i18n/use-i18n-standalone.svelte.ts +184 -0
- 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
|