From 9176a99e67bb396d5bd5c5e6b3d147d5ec5cd89c Mon Sep 17 00:00:00 2001 From: David Nuescheler Date: Wed, 15 Jul 2026 13:54:47 -0700 Subject: [PATCH 1/2] fix: load oversight URL thumbnails only after successful probe Avoid broken og:image placeholders by probing _ogimage before inserting img tags, skip thumbnails when the 404 checkpoint is filtered, and keep favicon fallback for facets that opt in. Co-authored-by: Cursor --- .../tools/optel/oversight/link-facet.test.js | 88 ++++++++++++++++++- tools/optel/oversight/elements/link-facet.js | 83 +++++++++++++---- 2 files changed, 155 insertions(+), 16 deletions(-) diff --git a/tests/tools/optel/oversight/link-facet.test.js b/tests/tools/optel/oversight/link-facet.test.js index 98bc2063..4d2f3976 100644 --- a/tests/tools/optel/oversight/link-facet.test.js +++ b/tests/tools/optel/oversight/link-facet.test.js @@ -1,6 +1,13 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { labelURLParts } from '../../../../tools/optel/oversight/elements/link-facet.js'; +import { + appendThumbnail, + getFaviconUrl, + getOgImageUrl, + is404CheckpointActive, + isThumbnailUrl, + labelURLParts, +} from '../../../../tools/optel/oversight/elements/link-facet.js'; describe('optel/oversight: labelURLParts', () => { it('renders parts for a https url', () => { @@ -39,3 +46,82 @@ describe('optel/oversight: labelURLParts', () => { ); }); }); + +describe('optel/oversight: thumbnail helpers', () => { + class MockImage { + constructor() { + this.onload = null; + this.onerror = null; + this.srcValue = ''; + MockImage.instances.push(this); + } + + set src(value) { + this.srcValue = value; + } + + get src() { + return this.srcValue; + } + } + + MockImage.instances = []; + + const createContainer = () => ({ children: [], prepend(node) { this.children.unshift(node); } }); + + it('detects thumbnail-eligible urls', () => { + assert.equal(isThumbnailUrl('https://example.com/path'), true); + assert.equal(isThumbnailUrl('http://example.com/path'), true); + assert.equal(isThumbnailUrl('android-app://com.example/app'), true); + assert.equal(isThumbnailUrl('/products/foo'), false); + }); + + it('builds og image proxy urls', () => { + const url = getOgImageUrl('https://example.com/page'); + assert.match(url, /^https:\/\/www\.aem\.live\/tools\/rum\/_ogimage\?/); + assert.match(url, /proxyurl=https%3A%2F%2Fexample\.com%2Fpage/); + }); + + it('builds favicon fallback urls', () => { + const url = getFaviconUrl('https://example.com/page'); + assert.match(url, /^https:\/\/www\.google\.com\/s2\/favicons\?/); + assert.match(url, /domain=https%3A%2F%2Fexample\.com%2Fpage/); + }); + + it('skips thumbnails when the 404 checkpoint is active', () => { + assert.equal(is404CheckpointActive({ href: 'https://tools.aem.live/tools/optel/oversight/explorer.html?checkpoint=404' }), true); + assert.equal(is404CheckpointActive({ href: 'https://tools.aem.live/tools/optel/oversight/explorer.html?checkpoint=enter&checkpoint=404' }), true); + assert.equal(is404CheckpointActive({ href: 'https://tools.aem.live/tools/optel/oversight/explorer.html?checkpoint=enter' }), false); + }); + + it('appends an image only after a successful probe', () => { + MockImage.instances = []; + const container = createContainer(); + appendThumbnail(container, 'https://example.com/page', { ImageCtor: MockImage }); + assert.equal(container.children.length, 0); + MockImage.instances[0].onload(); + assert.equal(container.children.length, 1); + assert.equal(container.children[0].tagName, 'IMG'); + assert.equal(container.children[0].alt, ''); + assert.equal(container.children[0].src, getOgImageUrl('https://example.com/page')); + }); + + it('falls back to favicon when og image probe fails', () => { + MockImage.instances = []; + const container = createContainer(); + appendThumbnail(container, 'https://example.com/page', { favicon: true, ImageCtor: MockImage }); + MockImage.instances[0].onerror(); + MockImage.instances[1].onload(); + assert.equal(container.children.length, 1); + assert.equal(container.children[0].className, 'favicon'); + assert.equal(container.children[0].src, getFaviconUrl('https://example.com/page')); + }); + + it('does not append an image when og image probe fails without favicon fallback', () => { + MockImage.instances = []; + const container = createContainer(); + appendThumbnail(container, 'https://example.com/page', { ImageCtor: MockImage }); + MockImage.instances[0].onerror(); + assert.equal(container.children.length, 0); + }); +}); diff --git a/tools/optel/oversight/elements/link-facet.js b/tools/optel/oversight/elements/link-facet.js index decce461..e58a285c 100644 --- a/tools/optel/oversight/elements/link-facet.js +++ b/tools/optel/oversight/elements/link-facet.js @@ -27,6 +27,56 @@ export function labelURLParts(url, prefix, solo = false) { } } +export function isThumbnailUrl(labelText) { + return labelText.startsWith('http://') + || labelText.startsWith('https://') + || labelText.startsWith('android-app://'); +} + +export function is404CheckpointActive(location = typeof window !== 'undefined' ? window.location : null) { + if (!location?.href) return false; + return new URL(location.href).searchParams.getAll('checkpoint').includes('404'); +} + +export function getOgImageUrl(labelText) { + const u = new URL('https://www.aem.live/tools/rum/_ogimage'); + u.searchParams.set('proxyurl', labelText); + return u.href; +} + +export function getFaviconUrl(labelText) { + return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(labelText)}&sz=256`; +} + +export function appendThumbnail(container, labelText, { favicon = false, ImageCtor = Image } = {}) { + const ogUrl = getOgImageUrl(labelText); + const probe = new ImageCtor(); + probe.onload = () => { + const img = document.createElement('img'); + img.loading = 'lazy'; + img.src = ogUrl; + img.title = labelText; + img.alt = ''; + container.prepend(img); + }; + probe.onerror = () => { + if (!favicon) return; + const favUrl = getFaviconUrl(labelText); + const favProbe = new ImageCtor(); + favProbe.onload = () => { + const img = document.createElement('img'); + img.loading = 'lazy'; + img.src = favUrl; + img.title = labelText; + img.alt = ''; + img.classList.add('favicon'); + container.prepend(img); + }; + favProbe.src = favUrl; + }; + probe.src = ogUrl; +} + /** * A custom HTML element to display a list of facets with links. * @@ -34,10 +84,24 @@ export function labelURLParts(url, prefix, solo = false) { * */ export default class LinkFacet extends ListFacet { + createValueSpan(entry, prefix, solo = false) { + const valuespan = super.createValueSpan(entry, prefix, solo); + this.maybeLoadThumbnail(valuespan, entry.value); + return valuespan; + } + + maybeLoadThumbnail(container, labelText) { + if (this.getAttribute('thumbnail') !== 'true') return; + if (is404CheckpointActive()) return; + if (!isThumbnailUrl(labelText)) return; + appendThumbnail(container, labelText, { + favicon: this.getAttribute('favicon') === 'true', + }); + } + // eslint-disable-next-line class-methods-use-this createLabelHTML(labelText, prefix, solo = false) { const thumbnailAtt = this.getAttribute('thumbnail') === 'true'; - const faviconAtt = this.getAttribute('favicon') === 'true'; const isCensored = labelText.includes('...') || labelText.includes('') || labelText.includes('%3Cnumber%3E') || labelText.includes('') || labelText.includes('%3Chex%3E') @@ -46,22 +110,11 @@ export default class LinkFacet extends ListFacet { if (isCensored) { return labelURLParts(labelText, prefix, solo); } - if (thumbnailAtt && labelText.startsWith('https://')) { - const u = new URL('https://www.aem.live/tools/rum/_ogimage'); - u.searchParams.set('proxyurl', labelText); - return ` - thumbnail image for ${escapeHTML(labelText)} - ${labelURLParts(labelText, prefix, solo)}`; - } - if (thumbnailAtt && (labelText.startsWith('http://') || labelText.startsWith('https://') || labelText.startsWith('android-app://'))) { - const u = new URL('https://www.aem.live/tools/rum/_ogimage'); - u.searchParams.set('proxyurl', labelText); - return ` - thumbnail image for ${escapeHTML(labelText)} - ${labelURLParts(labelText, prefix, solo)}`; + if (thumbnailAtt && isThumbnailUrl(labelText)) { + return `${labelURLParts(labelText, prefix, solo)}`; } if (labelText.startsWith('https://') || labelText.startsWith('http://')) { - return `${escapeHTML(labelText)}`; + return `${labelURLParts(labelText, prefix, solo)}`; } if (labelText.startsWith('referrer:')) { return `${escapeHTML(labelText.replace('referrer:', ''))}`; From ca2ad3c085aad4db0c51d37f4c86c7d78d965c97 Mon Sep 17 00:00:00 2001 From: Lars Trieloff Date: Thu, 23 Jul 2026 13:16:57 +0200 Subject: [PATCH 2/2] fix: keep full URL text for non-thumbnail link facets Revert the createLabelHTML non-thumbnail http(s):// branch back to escapeHTML(labelText) instead of labelURLParts(). The global CSS hides .hostname for link-facets, so decomposing URLs there would silently drop the hostname from facets like click.target, error.source and redirect.source. Keeps this PR scoped to the thumbnail-probe fix. --- tools/optel/oversight/elements/link-facet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/optel/oversight/elements/link-facet.js b/tools/optel/oversight/elements/link-facet.js index e58a285c..dd3f0d14 100644 --- a/tools/optel/oversight/elements/link-facet.js +++ b/tools/optel/oversight/elements/link-facet.js @@ -114,7 +114,7 @@ export default class LinkFacet extends ListFacet { return `${labelURLParts(labelText, prefix, solo)}`; } if (labelText.startsWith('https://') || labelText.startsWith('http://')) { - return `${labelURLParts(labelText, prefix, solo)}`; + return `${escapeHTML(labelText)}`; } if (labelText.startsWith('referrer:')) { return `${escapeHTML(labelText.replace('referrer:', ''))}`;