Skip to content

Commit 2e1018d

Browse files
committed
refactor(browser): drop maskPlaceholder, hardcode '***' default
No comparable browser RUM SDK (Sentry, Datadog) exposes a custom mask placeholder, and the OpenTelemetry HTTP semantic conventions hardcode 'REDACTED' for sensitive query params. Removing maskPlaceholder simplifies the API surface; the default '***' covers the realistic use cases. The option can be reintroduced non-breakingly later if needed.
1 parent 2c6e71b commit 2e1018d

7 files changed

Lines changed: 24 additions & 64 deletions

File tree

.changeset/mask-network-fields.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
---
55

66
Browser SDK: support masking sensitive fields in captured request/response
7-
headers and bodies before telemetry leaves the client. Add `maskFields` and
8-
`maskPlaceholder` options to `HyperDX.init`. Header matches are
9-
case-insensitive; body matches traverse nested JSON objects and accept dotted
10-
paths (e.g. `creditCard.number`).
7+
headers and bodies before telemetry leaves the client. Add a `maskFields`
8+
option to `HyperDX.init`. Header matches are case-insensitive; body matches
9+
traverse nested JSON objects and accept dotted paths (e.g.
10+
`creditCard.number`). Matched values are replaced with `***`.

packages/browser/README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ HyperDX.init({
4040
`advancedNetworkCapture` is enabled. Header matches are case-insensitive.
4141
Body matches walk through nested JSON objects and accept dotted paths to
4242
target nested properties (e.g. `creditCard.number`). Non-JSON request/
43-
response bodies are passed through unchanged. Example:
43+
response bodies are passed through unchanged. Matched values are replaced
44+
with `***`. Example:
4445
```js
4546
HyperDX.init({
4647
apiKey: '<YOUR_API_KEY_HERE>',
@@ -50,11 +51,8 @@ HyperDX.init({
5051
headers: ['authorization', 'x-api-key'],
5152
body: ['password', 'creditCard.number'],
5253
},
53-
maskPlaceholder: '***',
5454
});
5555
```
56-
- `maskPlaceholder` - (Optional) Replacement value used when a field matches
57-
`maskFields`. Defaults to `'***'`.
5856
- `url` - (Optional) The OpenTelemetry collector URL, only needed for
5957
self-hosted instances.
6058
- `maskAllInputs` - (Optional) Whether to mask all input fields in session

packages/browser/src/index.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,10 @@ type BrowserSDKConfig = {
4242
/**
4343
* Sensitive field names to mask in captured request/response headers and
4444
* bodies before telemetry leaves the browser. Only applies when
45-
* `advancedNetworkCapture` is enabled.
46-
*/
47-
maskFields?: MaskFields;
48-
/**
49-
* Replacement value used when a field matches `maskFields`. Defaults to
45+
* `advancedNetworkCapture` is enabled. Matched values are replaced with
5046
* `'***'`.
5147
*/
52-
maskPlaceholder?: string;
48+
maskFields?: MaskFields;
5349
recordCanvas?: boolean;
5450
sampling?: RumRecorderConfig['sampling'];
5551
service: string;
@@ -70,7 +66,6 @@ function hasWindow() {
7066
class Browser {
7167
private _advancedNetworkCapture = false;
7268
private _maskFields: MaskFields | undefined;
73-
private _maskPlaceholder: string | undefined;
7469

7570
init({
7671
advancedNetworkCapture = false,
@@ -88,7 +83,6 @@ class Browser {
8883
maskAllText = false,
8984
maskClass,
9085
maskFields,
91-
maskPlaceholder,
9286
recordCanvas = false,
9387
sampling,
9488
service,
@@ -120,7 +114,6 @@ class Browser {
120114

121115
this._advancedNetworkCapture = advancedNetworkCapture;
122116
this._maskFields = maskFields;
123-
this._maskPlaceholder = maskPlaceholder;
124117

125118
Rum.init({
126119
debug,
@@ -141,7 +134,6 @@ class Browser {
141134
: {}),
142135
advancedNetworkCapture: () => this._advancedNetworkCapture,
143136
maskFields: () => this._maskFields,
144-
maskPlaceholder: () => this._maskPlaceholder,
145137
},
146138
xhr: {
147139
...(tracePropagationTargets != null
@@ -151,7 +143,6 @@ class Browser {
151143
: {}),
152144
advancedNetworkCapture: () => this._advancedNetworkCapture,
153145
maskFields: () => this._maskFields,
154-
maskPlaceholder: () => this._maskPlaceholder,
155146
},
156147
...instrumentations,
157148
},

packages/otel-web/src/HyperDXFetchInstrumentation.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { headerCapture, maskBody, MaskFieldsConfig } from './utils';
99
export type HyperDXFetchInstrumentationConfig = FetchInstrumentationConfig & {
1010
advancedNetworkCapture?: () => boolean;
1111
maskFields?: () => MaskFieldsConfig | undefined;
12-
maskPlaceholder?: () => string | undefined;
1312
};
1413

1514
// not used yet
@@ -45,12 +44,10 @@ export class HyperDXFetchInstrumentation extends FetchInstrumentation {
4544

4645
if (config.advancedNetworkCapture?.() && span) {
4746
const maskFields = config.maskFields?.();
48-
const maskPlaceholder = config.maskPlaceholder?.();
4947

5048
if (request.headers) {
5149
headerCapture('request', Object.keys(request.headers), {
5250
maskFields: maskFields?.headers,
53-
maskPlaceholder,
5451
})(span, (header) => request.headers?.[header]);
5552
}
5653
if (request.body) {
@@ -63,7 +60,7 @@ export class HyperDXFetchInstrumentation extends FetchInstrumentation {
6360
} else {
6461
span.setAttribute(
6562
'http.request.body',
66-
maskBody(request.body, maskFields?.body, maskPlaceholder),
63+
maskBody(request.body, maskFields?.body),
6764
);
6865
}
6966
}
@@ -76,7 +73,6 @@ export class HyperDXFetchInstrumentation extends FetchInstrumentation {
7673
});
7774
headerCapture('response', headerNames, {
7875
maskFields: maskFields?.headers,
79-
maskPlaceholder,
8076
})(span, (header) => response.headers.get(header) ?? '');
8177
}
8278
response
@@ -85,7 +81,7 @@ export class HyperDXFetchInstrumentation extends FetchInstrumentation {
8581
.then((body) => {
8682
span.setAttribute(
8783
'http.response.body',
88-
maskBody(body, maskFields?.body, maskPlaceholder),
84+
maskBody(body, maskFields?.body),
8985
);
9086
})
9187
.catch(() => {

packages/otel-web/src/HyperDXXMLHttpRequestInstrumentation.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export type HyperDXXMLHttpRequestInstrumentationConfig =
2121
XMLHttpRequestInstrumentationConfig & {
2222
advancedNetworkCapture?: () => boolean;
2323
maskFields?: () => MaskFieldsConfig | undefined;
24-
maskPlaceholder?: () => string | undefined;
2524
};
2625

2726
export class HyperDXXMLHttpRequestInstrumentation extends XMLHttpRequestInstrumentation {
@@ -39,14 +38,12 @@ export class HyperDXXMLHttpRequestInstrumentation extends XMLHttpRequestInstrume
3938
if (config.advancedNetworkCapture?.()) {
4039
xhr.addEventListener('readystatechange', function () {
4140
const maskFields = config.maskFields?.();
42-
const maskPlaceholder = config.maskPlaceholder?.();
4341

4442
if (xhr.readyState === xhr.OPENED) {
4543
shimmer.wrap(xhr, 'setRequestHeader', (original) => {
4644
return function (header, value) {
4745
headerCapture('request', [header], {
4846
maskFields: maskFields?.headers,
49-
maskPlaceholder,
5047
})(span, () => value);
5148
return original.apply(this, arguments);
5249
};
@@ -56,7 +53,7 @@ export class HyperDXXMLHttpRequestInstrumentation extends XMLHttpRequestInstrume
5653
if (body) {
5754
span.setAttribute(
5855
'http.request.body',
59-
maskBody(body, maskFields?.body, maskPlaceholder),
56+
maskBody(body, maskFields?.body),
6057
);
6158
}
6259
return original.apply(this, arguments);
@@ -75,12 +72,11 @@ export class HyperDXXMLHttpRequestInstrumentation extends XMLHttpRequestInstrume
7572
}, {});
7673
headerCapture('response', Object.keys(headers), {
7774
maskFields: maskFields?.headers,
78-
maskPlaceholder,
7975
})(span, (header) => headers[header]);
8076
try {
8177
span.setAttribute(
8278
'http.response.body',
83-
maskBody(xhr.responseText, maskFields?.body, maskPlaceholder),
79+
maskBody(xhr.responseText, maskFields?.body),
8480
);
8581
} catch (e) {
8682
// ignore (DOMException if responseType is not the empty string or "text")

packages/otel-web/src/utils.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,14 @@ export function shouldMaskHeader(
232232
}
233233

234234
/**
235-
* Mask matching fields inside a JSON-shaped request/response body. When the
236-
* body cannot be parsed as JSON the original string is returned unchanged.
237-
* Field paths support dotted notation (e.g. `creditCard.number`).
235+
* Mask matching fields inside a JSON-shaped request/response body. Matched
236+
* values are replaced with `DEFAULT_MASK_PLACEHOLDER`. When the body cannot
237+
* be parsed as JSON the original string is returned unchanged. Field paths
238+
* support dotted notation (e.g. `creditCard.number`).
238239
*/
239240
export function maskBody(
240241
body: unknown,
241242
fieldsToMask: string[] | undefined,
242-
placeholder: string = DEFAULT_MASK_PLACEHOLDER,
243243
): string {
244244
const original = typeof body === 'string' ? body : jsonToString(body);
245245

@@ -256,7 +256,7 @@ export function maskBody(
256256
return original;
257257
}
258258

259-
const masked = maskJsonValue(parsed, fieldsToMask, placeholder);
259+
const masked = maskJsonValue(parsed, fieldsToMask);
260260

261261
try {
262262
return JSON.stringify(masked);
@@ -267,18 +267,17 @@ export function maskBody(
267267

268268
/**
269269
* Recursively walk a parsed JSON value and replace any property whose path
270-
* matches one of `fieldsToMask` with `placeholder`. Path comparison uses
271-
* dotted notation rooted at the top-level value.
270+
* matches one of `fieldsToMask` with `DEFAULT_MASK_PLACEHOLDER`. Path
271+
* comparison uses dotted notation rooted at the top-level value.
272272
*/
273273
function maskJsonValue(
274274
value: unknown,
275275
fieldsToMask: string[],
276-
placeholder: string,
277276
currentPath = '',
278277
): unknown {
279278
if (Array.isArray(value)) {
280279
return value.map((item) =>
281-
maskJsonValue(item, fieldsToMask, placeholder, currentPath),
280+
maskJsonValue(item, fieldsToMask, currentPath),
282281
);
283282
}
284283

@@ -287,12 +286,11 @@ function maskJsonValue(
287286
for (const key of Object.keys(value as Record<string, unknown>)) {
288287
const nextPath = currentPath ? `${currentPath}.${key}` : key;
289288
if (matchesField(key, nextPath, fieldsToMask)) {
290-
result[key] = placeholder;
289+
result[key] = DEFAULT_MASK_PLACEHOLDER;
291290
} else {
292291
result[key] = maskJsonValue(
293292
(value as Record<string, unknown>)[key],
294293
fieldsToMask,
295-
placeholder,
296294
nextPath,
297295
);
298296
}
@@ -323,12 +321,11 @@ function matchesField(
323321
export function headerCapture(
324322
type: 'request' | 'response',
325323
headers: string[],
326-
options: { maskFields?: string[]; maskPlaceholder?: string } = {},
324+
options: { maskFields?: string[] } = {},
327325
) {
328326
const normalizedHeaders = new Map(
329327
headers.map((header) => [header, header.toLowerCase().replace(/-/g, '_')]),
330328
);
331-
const placeholder = options.maskPlaceholder ?? DEFAULT_MASK_PLACEHOLDER;
332329

333330
return (
334331
span: Span,
@@ -347,9 +344,9 @@ export function headerCapture(
347344
let value: string | string[] | number = rawValue;
348345
if (masked) {
349346
if (Array.isArray(rawValue)) {
350-
value = rawValue.map(() => placeholder);
347+
value = rawValue.map(() => DEFAULT_MASK_PLACEHOLDER);
351348
} else {
352-
value = placeholder;
349+
value = DEFAULT_MASK_PLACEHOLDER;
353350
}
354351
}
355352

packages/otel-web/test/masking.test.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,6 @@ describe('maskBody', () => {
9393
});
9494
});
9595

96-
it('uses a custom placeholder when provided', () => {
97-
const body = JSON.stringify({ password: 'secret' });
98-
const masked = JSON.parse(maskBody(body, ['password'], '[REDACTED]'));
99-
assert.deepStrictEqual(masked, { password: '[REDACTED]' });
100-
});
101-
10296
it('returns the original body unchanged when it is not JSON', () => {
10397
const body = 'username=alice&password=secret';
10498
assert.strictEqual(maskBody(body, ['password']), body);
@@ -140,18 +134,6 @@ describe('headerCapture with masking', () => {
140134
]);
141135
});
142136

143-
it('honors a custom mask placeholder', () => {
144-
const span = makeFakeSpan();
145-
headerCapture('request', ['x-api-key'], {
146-
maskFields: ['x-api-key'],
147-
maskPlaceholder: '[REDACTED]',
148-
})(span as any, () => 'super-secret-token');
149-
150-
assert.deepStrictEqual(span.attributes['http.request.header.x_api_key'], [
151-
'[REDACTED]',
152-
]);
153-
});
154-
155137
it('matches header names case-insensitively', () => {
156138
const span = makeFakeSpan();
157139
headerCapture('request', ['Authorization'], {

0 commit comments

Comments
 (0)