From 152e358efd7100be4e6bb2ca7fe2ec3643fb5b91 Mon Sep 17 00:00:00 2001 From: Bastien Caudan <1331991+bcaudan@users.noreply.github.com> Date: Tue, 16 Jan 2024 09:32:44 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B[RUM-2752]=20Replay:=20generate=20c?= =?UTF-8?q?ensored=20images=20with=20custom=20dimensions=20(#2565)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replay: generate censored image with similar dimension than the original * 👌 add extra fallback --- .../serialization/serializationUtils.ts | 4 ++ .../serialization/serializeAttribute.spec.ts | 42 +++++++++++++++++++ .../serialization/serializeAttribute.ts | 22 ++++++++-- 3 files changed, 65 insertions(+), 3 deletions(-) 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