diff --git a/packages/rum/src/domain/record/serialization/serializationUtils.ts b/packages/rum/src/domain/record/serialization/serializationUtils.ts index 5a8bf45679..218aa564b2 100644 --- a/packages/rum/src/domain/record/serialization/serializationUtils.ts +++ b/packages/rum/src/domain/record/serialization/serializationUtils.ts @@ -119,3 +119,7 @@ export function getValidTagName(tagName: string): string { return processedTagName } + +export function censoredImageForSize(width: number, height: number) { + return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='${width}' height='${height}' style='background-color:silver'%3E%3C/svg%3E` +} diff --git a/packages/rum/src/domain/record/serialization/serializeAttribute.spec.ts b/packages/rum/src/domain/record/serialization/serializeAttribute.spec.ts index e3f10747cf..d1cd214f05 100644 --- a/packages/rum/src/domain/record/serialization/serializeAttribute.spec.ts +++ b/packages/rum/src/domain/record/serialization/serializeAttribute.spec.ts @@ -96,4 +96,46 @@ describe('serializeAttribute', () => { expect(serializeAttribute(node, NodePrivacyLevel.MASK, STABLE_ATTRIBUTES[0], DEFAULT_CONFIGURATION)).toBe('foo') }) }) + + describe('image masking', () => { + let imageStub: Partial & { width: number; height: number; naturalWidth: number; naturalHeight: number } + + beforeEach(() => { + imageStub = { + width: 0, + height: 0, + naturalWidth: 0, + naturalHeight: 0, + tagName: 'IMG', + getAttribute() { + return 'http://foo.bar/image.png' + }, + getBoundingClientRect() { + return { width: this.width, height: this.height } as DOMRect + }, + } + }) + + it('should use an image with same natural dimension than the original one', () => { + imageStub.naturalWidth = 2000 + imageStub.naturalHeight = 1000 + expect(serializeAttribute(imageStub as Element, NodePrivacyLevel.MASK, 'src', DEFAULT_CONFIGURATION)).toBe( + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2000' height='1000' style='background-color:silver'%3E%3C/svg%3E" + ) + }) + + it('should use an image with same rendering dimension than the original one', () => { + imageStub.width = 200 + imageStub.height = 100 + expect(serializeAttribute(imageStub as Element, NodePrivacyLevel.MASK, 'src', DEFAULT_CONFIGURATION)).toBe( + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='100' style='background-color:silver'%3E%3C/svg%3E" + ) + }) + + it("should use the censored image when original image size can't be computed", () => { + expect(serializeAttribute(imageStub as Element, NodePrivacyLevel.MASK, 'src', DEFAULT_CONFIGURATION)).toBe( + 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==' + ) + }) + }) }) diff --git a/packages/rum/src/domain/record/serialization/serializeAttribute.ts b/packages/rum/src/domain/record/serialization/serializeAttribute.ts index 6fb0450731..f04d74262d 100644 --- a/packages/rum/src/domain/record/serialization/serializeAttribute.ts +++ b/packages/rum/src/domain/record/serialization/serializeAttribute.ts @@ -3,6 +3,7 @@ import { STABLE_ATTRIBUTES } from '@datadog/browser-rum-core' import type { RumConfiguration } from '@datadog/browser-rum-core' import { NodePrivacyLevel, PRIVACY_ATTR_NAME, CENSORED_STRING_MARK, CENSORED_IMG_MARK } from '../../../constants' import { MAX_ATTRIBUTE_VALUE_CHAR_LENGTH } from '../privacy' +import { censoredImageForSize } from './serializationUtils' export function serializeAttribute( element: Element, @@ -30,12 +31,27 @@ export function serializeAttribute( case 'placeholder': return CENSORED_STRING_MARK } + // mask image URLs - if (tagName === 'IMG' || tagName === 'SOURCE') { - if (attributeName === 'src' || attributeName === 'srcset') { - return CENSORED_IMG_MARK + if (tagName === 'IMG' && (attributeName === 'src' || attributeName === 'srcset')) { + // generate image with similar dimension than the original to have the same rendering behaviour + const image = element as HTMLImageElement + if (image.naturalWidth > 0) { + return censoredImageForSize(image.naturalWidth, image.naturalHeight) + } + const { width, height } = element.getBoundingClientRect() + if (width > 0 || height > 0) { + return censoredImageForSize(width, height) } + // if we can't get the image size, fallback to the censored image + return CENSORED_IMG_MARK } + + // mask source URLs + if (tagName === 'SOURCE' && (attributeName === 'src' || attributeName === 'srcset')) { + return CENSORED_IMG_MARK + } + // mask URLs if (tagName === 'A' && attributeName === 'href') { return CENSORED_STRING_MARK