Skip to content

Commit d4cb5fb

Browse files
Split support bundle browser telemetry module
1 parent 85889f7 commit d4cb5fb

11 files changed

Lines changed: 204 additions & 113 deletions

archive/folderview.plus-2026.04.02.09.txz.sha256

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
48d4044656e2adb5d3ec338b8a42fc78a86507ddc0a8dab4a332040e226d56ec folderview.plus-2026.04.04.18.txz

folderview.plus.plg

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66
<!ENTITY launch "Settings/FolderViewPlus">
77
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
88
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/dev/folderview.plus.plg">
9-
<!ENTITY version "2026.04.04.17">
10-
<!ENTITY md5 "a71f344882e7619a56b292ef58944314">
9+
<!ENTITY version "2026.04.04.18">
10+
<!ENTITY md5 "f70091149cd0f7869613fd67ad1adfed">
1111
]>
1212

1313
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;" pluginURL="&pluginURL;" icon="folder-icon.png" support="https://forums.unraid.net/topic/197631-plugin-folderview-plus/" min="7.0.0">
1414
<CHANGES>
1515

16+
###2026.04.04.18
17+
- Feature: Support bundle v2 now exports exact build identity, packaged manifest checksums, source commit metadata, loaded plugin assets, recent plugin actions, plugin-scoped API error log tails, and browser-side FolderView Plus error snapshots.
18+
- Security: Sanitized support bundles redact the new browser and server troubleshooting fields through the shared redaction pipeline while preserving correlation via per-export hashed values.
19+
- Quality: Split support-bundle browser collectors into a dedicated settings module and updated include-order, perf-budget, and diagnostics regression guards for the expanded export surface.
20+
21+
1622
###2026.04.04.17
1723
- UX: Settings workspace layout, section flows, and table behavior.
1824
- Fix: Diagnostics surfaces, issue reports, and support bundle coverage.

scripts/include_order_guard.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const expectedOrder = [
3535
'folderviewplus.smart-detect-config.js',
3636
'folderviewplus.starter-templates.js',
3737
'folderviewplus.support-bundle-preview.js',
38+
'folderviewplus.support-bundle-browser.js',
3839
'folderviewplus.support-bundle-telemetry.js',
3940
'folderviewplus.activity-diagnostics.js',
4041
'folderviewplus.settings-tree.js',

scripts/perf_budget_guard.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ const budgets = [
9898
maxBytes: envInt('FVPLUS_MAX_FOLDERVIEWPLUS_SUPPORT_BUNDLE_PREVIEW_JS_BYTES', 25000),
9999
maxGzipBytes: envInt('FVPLUS_MAX_FOLDERVIEWPLUS_SUPPORT_BUNDLE_PREVIEW_JS_GZIP_BYTES', 7000),
100100
},
101+
{
102+
path: 'scripts/folderviewplus.support-bundle-browser.js',
103+
maxBytesEnv: 'FVPLUS_MAX_FOLDERVIEWPLUS_SUPPORT_BUNDLE_BROWSER_JS_BYTES',
104+
maxGzipBytesEnv: 'FVPLUS_MAX_FOLDERVIEWPLUS_SUPPORT_BUNDLE_BROWSER_JS_GZIP_BYTES',
105+
maxBytes: envInt('FVPLUS_MAX_FOLDERVIEWPLUS_SUPPORT_BUNDLE_BROWSER_JS_BYTES', 12000),
106+
maxGzipBytes: envInt('FVPLUS_MAX_FOLDERVIEWPLUS_SUPPORT_BUNDLE_BROWSER_JS_GZIP_BYTES', 3500),
107+
},
101108
{
102109
path: 'scripts/folderviewplus.support-bundle-telemetry.js',
103110
maxBytesEnv: 'FVPLUS_MAX_FOLDERVIEWPLUS_SUPPORT_BUNDLE_TELEMETRY_JS_BYTES',
@@ -234,6 +241,7 @@ const settingsRuntimePaths = [
234241
'scripts/folderviewplus.smart-detect-config.js',
235242
'scripts/folderviewplus.starter-templates.js',
236243
'scripts/folderviewplus.support-bundle-preview.js',
244+
'scripts/folderviewplus.support-bundle-browser.js',
237245
'scripts/folderviewplus.support-bundle-telemetry.js',
238246
'scripts/folderviewplus.activity-diagnostics.js',
239247
'scripts/folderviewplus.settings-tree.js',

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/FolderViewPlus.page

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,7 @@ if (!empty($fvplusRuntimeConflicts)) {
13991399
<script src="<?=autov('/plugins/folderview.plus/scripts/folderviewplus.smart-detect-config.js')?>"></script>
14001400
<script src="<?=autov('/plugins/folderview.plus/scripts/folderviewplus.starter-templates.js')?>"></script>
14011401
<script src="<?=autov('/plugins/folderview.plus/scripts/folderviewplus.support-bundle-preview.js')?>"></script>
1402+
<script src="<?=autov('/plugins/folderview.plus/scripts/folderviewplus.support-bundle-browser.js')?>"></script>
14021403
<script src="<?=autov('/plugins/folderview.plus/scripts/folderviewplus.support-bundle-telemetry.js')?>"></script>
14031404
<script src="<?=autov('/plugins/folderview.plus/scripts/folderviewplus.activity-diagnostics.js')?>"></script>
14041405
<script src="<?=autov('/plugins/folderview.plus/scripts/folderviewplus.settings-tree.js')?>"></script>
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
(function(root, factory) {
2+
if (typeof module === 'object' && module.exports) {
3+
module.exports = factory(root);
4+
return;
5+
}
6+
root.FolderViewPlusSupportBundleBrowser = factory(root);
7+
root.FolderViewPlusSupportBundleBrowserModuleLoaded = true;
8+
}(typeof globalThis !== 'undefined' ? globalThis : this, function(root) {
9+
const CONSOLE_ERROR_STORAGE_KEY = 'fv.support.bundle.consoleErrors.v1';
10+
const CONSOLE_ERROR_LIMIT = 30;
11+
12+
const clientStorageIsAvailable = (kind) => {
13+
try {
14+
return typeof root?.[kind] !== 'undefined';
15+
} catch (_error) {
16+
return false;
17+
}
18+
};
19+
20+
const createCollectors = (deps = {}) => {
21+
const readClientDiagnosticsStorageRecord = typeof deps.readClientDiagnosticsStorageRecord === 'function'
22+
? deps.readClientDiagnosticsStorageRecord
23+
: (() => null);
24+
const storageKeys = deps.storageKeys && typeof deps.storageKeys === 'object' && !Array.isArray(deps.storageKeys)
25+
? deps.storageKeys
26+
: {};
27+
28+
const collectBrowserCapabilities = () => ({
29+
clipboardWrite: Boolean(root?.navigator?.clipboard && typeof root.navigator.clipboard.writeText === 'function'),
30+
cookieEnabled: root?.navigator?.cookieEnabled !== false,
31+
fetch: typeof root?.fetch === 'function',
32+
mutationObserver: typeof root?.MutationObserver === 'function',
33+
pointerEvent: typeof root?.PointerEvent === 'function',
34+
resizeObserver: typeof root?.ResizeObserver === 'function',
35+
touchPoints: Number.isFinite(Number(root?.navigator?.maxTouchPoints)) ? Number(root.navigator.maxTouchPoints) : 0,
36+
viewport: {
37+
width: Number.isFinite(Number(root?.innerWidth)) ? Number(root.innerWidth) : 0,
38+
height: Number.isFinite(Number(root?.innerHeight)) ? Number(root.innerHeight) : 0,
39+
devicePixelRatio: Number.isFinite(Number(root?.devicePixelRatio)) ? Number(root.devicePixelRatio) : 1
40+
}
41+
});
42+
43+
const collectClientStorageDiagnostics = () => ({
44+
localStorageAvailable: clientStorageIsAvailable('localStorage'),
45+
sessionStorageAvailable: clientStorageIsAvailable('sessionStorage'),
46+
folderEditorDebug: {
47+
launchPresent: Boolean(readClientDiagnosticsStorageRecord(storageKeys.launch || '')),
48+
bootstrapPresent: Boolean(readClientDiagnosticsStorageRecord(storageKeys.bootstrap || '')),
49+
surfacePresent: Boolean(readClientDiagnosticsStorageRecord(storageKeys.surface || ''))
50+
}
51+
});
52+
53+
const collectCurrentPageTelemetry = (uiRedactor) => {
54+
const href = String(root?.location?.href || '');
55+
return {
56+
path: String(root?.location?.pathname || ''),
57+
href: uiRedactor ? uiRedactor.redactUrl('uiTelemetry.currentPage.href', href) : href
58+
};
59+
};
60+
61+
const collectLoadedAssetTelemetry = (uiRedactor) => {
62+
const doc = root?.document || null;
63+
if (!doc || typeof doc.querySelectorAll !== 'function') {
64+
return { count: 0, entries: [] };
65+
}
66+
const entries = [];
67+
const seen = new Set();
68+
doc.querySelectorAll('script[src*="/plugins/folderview.plus/"], link[href*="/plugins/folderview.plus/"]').forEach((node) => {
69+
const rawUrl = String(node?.src || node?.href || '').trim();
70+
if (!rawUrl || seen.has(rawUrl)) {
71+
return;
72+
}
73+
seen.add(rawUrl);
74+
let pathname = rawUrl;
75+
let versionQuery = '';
76+
let bootQuery = '';
77+
try {
78+
const parsed = new URL(rawUrl, root?.location?.origin || 'http://fvplus.local');
79+
pathname = parsed.pathname || rawUrl;
80+
versionQuery = String(parsed.searchParams.get('v') || '');
81+
bootQuery = String(parsed.searchParams.get('boot') || '');
82+
} catch (_error) {
83+
pathname = rawUrl.replace(/^https?:\/\/[^/?#]+/i, '').replace(/[?#].*$/, '') || rawUrl;
84+
}
85+
entries.push({
86+
tag: String(node?.tagName || '').toLowerCase() || 'asset',
87+
url: uiRedactor ? uiRedactor.redactUrl(`uiTelemetry.loadedAssets.entries.${entries.length}.url`, rawUrl) : pathname,
88+
path: pathname,
89+
versionQuery,
90+
bootQuery,
91+
async: node?.async === true,
92+
defer: node?.defer === true,
93+
rel: String(node?.rel || ''),
94+
media: String(node?.media || ''),
95+
loaded: node?.tagName === 'LINK' ? Boolean(node.sheet) : true
96+
});
97+
});
98+
return {
99+
count: entries.length,
100+
entries
101+
};
102+
};
103+
104+
const collectBrowserConsoleErrors = () => {
105+
const fallbackStorage = readClientDiagnosticsStorageRecord(CONSOLE_ERROR_STORAGE_KEY);
106+
const apiSnapshot = (
107+
root?.FolderViewPlusFatalBanner
108+
&& typeof root.FolderViewPlusFatalBanner.getBrowserConsoleErrorSnapshot === 'function'
109+
)
110+
? root.FolderViewPlusFatalBanner.getBrowserConsoleErrorSnapshot()
111+
: null;
112+
const snapshot = apiSnapshot && typeof apiSnapshot === 'object' && !Array.isArray(apiSnapshot)
113+
? apiSnapshot
114+
: {
115+
storageKey: CONSOLE_ERROR_STORAGE_KEY,
116+
maxEntries: CONSOLE_ERROR_LIMIT,
117+
count: Array.isArray(fallbackStorage) ? fallbackStorage.length : 0,
118+
entries: Array.isArray(fallbackStorage) ? fallbackStorage : []
119+
};
120+
return {
121+
storageKey: String(snapshot.storageKey || CONSOLE_ERROR_STORAGE_KEY),
122+
maxEntries: Number.isFinite(Number(snapshot.maxEntries)) ? Number(snapshot.maxEntries) : CONSOLE_ERROR_LIMIT,
123+
count: Number.isFinite(Number(snapshot.count)) ? Number(snapshot.count) : 0,
124+
entries: Array.isArray(snapshot.entries) ? snapshot.entries.slice(-CONSOLE_ERROR_LIMIT) : []
125+
};
126+
};
127+
128+
return Object.freeze({
129+
collectBrowserCapabilities,
130+
collectClientStorageDiagnostics,
131+
collectCurrentPageTelemetry,
132+
collectLoadedAssetTelemetry,
133+
collectBrowserConsoleErrors
134+
});
135+
};
136+
137+
return Object.freeze({
138+
createCollectors,
139+
clientStorageIsAvailable,
140+
CONSOLE_ERROR_STORAGE_KEY,
141+
CONSOLE_ERROR_LIMIT
142+
});
143+
}));

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folderviewplus.support-bundle-telemetry.js

Lines changed: 27 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
root.FolderViewPlusSupportBundleTelemetry = factory(root);
77
root.FolderViewPlusSupportBundleTelemetryModuleLoaded = true;
88
}(typeof globalThis !== 'undefined' ? globalThis : this, function(root) {
9+
const browserModule = root?.FolderViewPlusSupportBundleBrowser || null;
910
const SUPPORT_BUNDLE_UI_ID_KEYS = Object.freeze(new Set([
1011
'id',
1112
'folderId',
@@ -51,9 +52,7 @@
5152
'responseSnippet'
5253
]));
5354

54-
const normalizePrivacyMode = (value) => (
55-
String(value || '').trim().toLowerCase() === 'full' ? 'full' : 'sanitized'
56-
);
55+
const normalizePrivacyMode = (value) => (String(value || '').trim().toLowerCase() === 'full' ? 'full' : 'sanitized');
5756

5857
const hashValue = (value, saltSeed = '') => {
5958
const input = `${String(saltSeed || '')}|${String(value || '')}`;
@@ -82,14 +81,6 @@
8281
manifest[safeBucket] = list;
8382
};
8483

85-
const clientStorageIsAvailable = (kind) => {
86-
try {
87-
return typeof root?.[kind] !== 'undefined';
88-
} catch (_error) {
89-
return false;
90-
}
91-
};
92-
9384
const createUiTelemetryRedactor = (bundle, privacy = 'sanitized') => {
9485
const mode = normalizePrivacyMode(privacy);
9586
const payload = bundle && typeof bundle === 'object' && !Array.isArray(bundle) ? bundle : {};
@@ -215,108 +206,37 @@
215206
? deps.storageKeys
216207
: {};
217208

218-
const collectBrowserCapabilities = () => ({
219-
clipboardWrite: Boolean(root?.navigator?.clipboard && typeof root.navigator.clipboard.writeText === 'function'),
220-
cookieEnabled: root?.navigator?.cookieEnabled !== false,
221-
fetch: typeof root?.fetch === 'function',
222-
mutationObserver: typeof root?.MutationObserver === 'function',
223-
pointerEvent: typeof root?.PointerEvent === 'function',
224-
resizeObserver: typeof root?.ResizeObserver === 'function',
225-
touchPoints: Number.isFinite(Number(root?.navigator?.maxTouchPoints)) ? Number(root.navigator.maxTouchPoints) : 0,
226-
viewport: {
227-
width: Number.isFinite(Number(root?.innerWidth)) ? Number(root.innerWidth) : 0,
228-
height: Number.isFinite(Number(root?.innerHeight)) ? Number(root.innerHeight) : 0,
229-
devicePixelRatio: Number.isFinite(Number(root?.devicePixelRatio)) ? Number(root.devicePixelRatio) : 1
230-
}
231-
});
232-
233-
const collectClientStorageDiagnostics = () => ({
234-
localStorageAvailable: clientStorageIsAvailable('localStorage'),
235-
sessionStorageAvailable: clientStorageIsAvailable('sessionStorage'),
209+
const browserCollectors = (
210+
browserModule
211+
&& typeof browserModule.createCollectors === 'function'
212+
) ? browserModule.createCollectors({
213+
readClientDiagnosticsStorageRecord,
214+
storageKeys
215+
}) : null;
216+
const collectBrowserCapabilities = browserCollectors?.collectBrowserCapabilities || (() => ({}));
217+
const collectClientStorageDiagnostics = browserCollectors?.collectClientStorageDiagnostics || (() => ({
218+
localStorageAvailable: false,
219+
sessionStorageAvailable: false,
236220
folderEditorDebug: {
237-
launchPresent: Boolean(readClientDiagnosticsStorageRecord(storageKeys.launch || '')),
238-
bootstrapPresent: Boolean(readClientDiagnosticsStorageRecord(storageKeys.bootstrap || '')),
239-
surfacePresent: Boolean(readClientDiagnosticsStorageRecord(storageKeys.surface || ''))
221+
launchPresent: false,
222+
bootstrapPresent: false,
223+
surfacePresent: false
240224
}
241-
});
242-
243-
const collectCurrentPageTelemetry = (uiRedactor) => {
244-
const pathname = String(root?.location?.pathname || '');
225+
}));
226+
const collectCurrentPageTelemetry = browserCollectors?.collectCurrentPageTelemetry || ((uiRedactor) => {
245227
const href = String(root?.location?.href || '');
246228
return {
247-
path: pathname,
229+
path: String(root?.location?.pathname || ''),
248230
href: uiRedactor ? uiRedactor.redactUrl('uiTelemetry.currentPage.href', href) : href
249231
};
250-
};
251-
252-
const collectLoadedAssetTelemetry = (uiRedactor) => {
253-
const doc = root?.document || null;
254-
if (!doc || typeof doc.querySelectorAll !== 'function') {
255-
return { count: 0, entries: [] };
256-
}
257-
const entries = [];
258-
const seen = new Set();
259-
doc.querySelectorAll('script[src*="/plugins/folderview.plus/"], link[href*="/plugins/folderview.plus/"]').forEach((node) => {
260-
const rawUrl = String(node?.src || node?.href || '').trim();
261-
if (!rawUrl || seen.has(rawUrl)) {
262-
return;
263-
}
264-
seen.add(rawUrl);
265-
let pathname = rawUrl;
266-
let versionQuery = '';
267-
let bootQuery = '';
268-
try {
269-
const parsed = new URL(rawUrl, root?.location?.origin || 'http://fvplus.local');
270-
pathname = parsed.pathname || rawUrl;
271-
versionQuery = String(parsed.searchParams.get('v') || '');
272-
bootQuery = String(parsed.searchParams.get('boot') || '');
273-
} catch (_error) {
274-
pathname = rawUrl.replace(/^https?:\/\/[^/?#]+/i, '').replace(/[?#].*$/, '') || rawUrl;
275-
}
276-
entries.push({
277-
tag: String(node?.tagName || '').toLowerCase() || 'asset',
278-
url: uiRedactor ? uiRedactor.redactUrl(`uiTelemetry.loadedAssets.entries.${entries.length}.url`, rawUrl) : pathname,
279-
path: pathname,
280-
versionQuery,
281-
bootQuery,
282-
async: node?.async === true,
283-
defer: node?.defer === true,
284-
rel: String(node?.rel || ''),
285-
media: String(node?.media || ''),
286-
loaded: node?.tagName === 'LINK'
287-
? Boolean(node.sheet)
288-
: true
289-
});
290-
});
291-
return {
292-
count: entries.length,
293-
entries
294-
};
295-
};
296-
297-
const collectBrowserConsoleErrors = () => {
298-
const fallbackStorage = readClientDiagnosticsStorageRecord('fv.support.bundle.consoleErrors.v1');
299-
const apiSnapshot = (
300-
root?.FolderViewPlusFatalBanner
301-
&& typeof root.FolderViewPlusFatalBanner.getBrowserConsoleErrorSnapshot === 'function'
302-
)
303-
? root.FolderViewPlusFatalBanner.getBrowserConsoleErrorSnapshot()
304-
: null;
305-
const snapshot = apiSnapshot && typeof apiSnapshot === 'object' && !Array.isArray(apiSnapshot)
306-
? apiSnapshot
307-
: {
308-
storageKey: 'fv.support.bundle.consoleErrors.v1',
309-
maxEntries: 30,
310-
count: Array.isArray(fallbackStorage) ? fallbackStorage.length : 0,
311-
entries: Array.isArray(fallbackStorage) ? fallbackStorage : []
312-
};
313-
return {
314-
storageKey: String(snapshot.storageKey || 'fv.support.bundle.consoleErrors.v1'),
315-
maxEntries: Number.isFinite(Number(snapshot.maxEntries)) ? Number(snapshot.maxEntries) : 30,
316-
count: Number.isFinite(Number(snapshot.count)) ? Number(snapshot.count) : 0,
317-
entries: Array.isArray(snapshot.entries) ? snapshot.entries.slice(-30) : []
318-
};
319-
};
232+
});
233+
const collectLoadedAssetTelemetry = browserCollectors?.collectLoadedAssetTelemetry || (() => ({ count: 0, entries: [] }));
234+
const collectBrowserConsoleErrors = browserCollectors?.collectBrowserConsoleErrors || (() => ({
235+
storageKey: 'fv.support.bundle.consoleErrors.v1',
236+
maxEntries: 30,
237+
count: 0,
238+
entries: []
239+
}));
320240

321241
const collectSupportBundleUiTelemetry = (bundle) => {
322242
const payload = normalizeSupportBundleV2Payload(bundle, bundle?.bundleMeta?.privacyMode || 'sanitized');

0 commit comments

Comments
 (0)