tracker: "secure by default" mode; 16.1.0

This commit is contained in:
nick-delirium 2025-03-14 16:32:26 +01:00 committed by Delirium
parent 4bac12308a
commit e7d309dadf
9 changed files with 31 additions and 13 deletions

View file

@ -1,3 +1,7 @@
## 16.1.0
- new `privateMode` option to hide all possible data from tracking
## 16.0.3
- better handling for local svg spritemaps

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "16.0.3",
"version": "16.1.0",
"keywords": [
"logging",
"replay"

View file

@ -357,6 +357,9 @@ export default abstract class Observer {
if (name === 'href' || value.length > 1e5) {
value = ''
}
if (['alt', 'placeholder'].includes(name) && this.app.sanitizer.privateMode) {
value = value.replaceAll(/./g, '*')
}
this.app.attributeSender.sendSetAttribute(id, name, value)
}

View file

@ -41,12 +41,13 @@ export interface Options {
export const stringWiper = (input: string) =>
input
.trim()
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '')
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '*')
export default class Sanitizer {
private readonly obscured: Set<number> = new Set()
private readonly hidden: Set<number> = new Set()
private readonly options: Options
public readonly privateMode: boolean
private readonly app: App
constructor(params: { app: App; options?: Partial<Options> }) {
@ -57,16 +58,17 @@ export default class Sanitizer {
privateMode: false,
domSanitizer: undefined,
}
this.privateMode = params.options?.privateMode ?? false
this.options = Object.assign(defaultOptions, params.options)
}
handleNode(id: number, parentID: number, node: Node) {
if (this.options.privateMode) {
if (isElementNode(node) && !hasOpenreplayAttribute(node, 'unmask')) {
this.obscured.add(id)
return this.obscured.add(id)
}
if (isTextNode(node) && !hasOpenreplayAttribute(node.parentNode as Element, 'unmask')) {
this.obscured.add(id)
return this.obscured.add(id)
}
}

View file

@ -108,9 +108,13 @@ export default function (app: App, opts: Partial<Options>): void {
return
}
const sendConsoleLog = app.safe((level: string, args: unknown[]): void =>
app.send(ConsoleLog(level, printf(args))),
)
const sendConsoleLog = app.safe((level: string, args: unknown[]): void => {
let logMsg = printf(args)
if (app.sanitizer.privateMode) {
logMsg = logMsg.replaceAll(/./g, '*')
}
app.send(ConsoleLog(level, logMsg))
})
let n = 0
const reset = (): void => {

View file

@ -205,7 +205,10 @@ export default function (app: App, opts: Partial<Options>): void {
inputTime: number,
) {
const { value, mask } = getInputValue(id, node)
const label = getInputLabel(node)
let label = getInputLabel(node)
if (app.sanitizer.privateMode) {
label = label.replaceAll(/./g, '*')
}
app.send(InputChange(id, value, mask !== 0, label, hesitationTime, inputTime))
}

View file

@ -230,11 +230,12 @@ export default function (app: App, options?: MouseHandlerOptions): void {
const normalizedY = roundNumber(clickY / contentHeight)
sendMouseMove()
const label = getTargetLabel(target)
app.send(
MouseClick(
id,
mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0,
getTargetLabel(target),
app.sanitizer.privateMode ? label.replaceAll(/./g, '*') : label,
isClickable(target) && !disableClickmaps ? getSelector(id, target, options) : '',
normalizedX,
normalizedY,

View file

@ -101,7 +101,7 @@ export default function (app: App, opts: Partial<Options> = {}) {
}
function sanitize(reqResInfo: RequestResponseData) {
if (!options.capturePayload) {
if (!options.capturePayload || app.sanitizer.privateMode) {
// @ts-ignore
delete reqResInfo.request.body
delete reqResInfo.response.body
@ -136,18 +136,19 @@ export default function (app: App, opts: Partial<Options> = {}) {
if (options.useProxy) {
return createNetworkProxy(
context,
options.ignoreHeaders,
app.sanitizer.privateMode ? true : options.ignoreHeaders,
setSessionTokenHeader,
sanitize,
(message) => {
if (options.failuresOnly && message.status < 400) {
return
}
const url = app.sanitizer.privateMode ? '************' : message.url
app.send(
NetworkRequest(
message.requestType,
message.method,
message.url,
url,
message.request,
message.response,
message.status,

View file

@ -147,7 +147,7 @@ export default function (app: App, opts: Partial<Options>): void {
entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0,
entry.encodedBodySize || 0,
entry.decodedBodySize || 0,
entry.name,
app.sanitizer.privateMode ? entry.name.replaceAll(/./g, '*') : entry.name,
entry.initiatorType,
entry.transferSize,
// @ts-ignore