Skip to content

Commit dab634d

Browse files
committed
sanitizer optional, export default with usage, fix tests
1 parent 9ba5069 commit dab634d

File tree

4 files changed

+89
-18
lines changed

4 files changed

+89
-18
lines changed

packages/instrumentation-browser-navigation/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ The instrumentation accepts the following configuration options:
6363
| ------ | ---- | ------- | ----------- |
6464
| `enabled` | `boolean` | `true` | Enable/disable the instrumentation |
6565
| `useNavigationApiIfAvailable` | `boolean` | `true` | Use the Navigation API when available for better accuracy |
66+
| `sanitizeUrl` | `function` | `undefined` | Callback to sanitize URLs before adding to log records |
6667
| `applyCustomLogRecordData` | `function` | `undefined` | Callback to add custom attributes to log records |
6768

6869
## Navigation API vs Traditional APIs
@@ -73,6 +74,68 @@ When `useNavigationApiIfAvailable` is `true` (default), the instrumentation will
7374
- **Fall back to traditional APIs** (history patching, popstate, etc.) in older browsers
7475
- **Prevent duplicate events** by using only one API set at a time
7576

77+
## URL Sanitization
78+
79+
**Important**: By default, URLs are **not sanitized** and will be recorded as-is. For security and privacy, you should provide a `sanitizeUrl` function to redact sensitive information.
80+
81+
### Using the Default Sanitizer
82+
83+
The package exports a `defaultSanitizeUrl` function that removes credentials and common sensitive query parameters:
84+
85+
```ts
86+
import { BrowserNavigationInstrumentation, defaultSanitizeUrl } from '@opentelemetry/instrumentation-browser-navigation';
87+
88+
registerInstrumentations({
89+
instrumentations: [
90+
new BrowserNavigationInstrumentation({
91+
sanitizeUrl: defaultSanitizeUrl,
92+
}),
93+
],
94+
});
95+
```
96+
97+
The default sanitizer redacts:
98+
- **Credentials**: `https://user:[email protected]``https://REDACTED:[email protected]`
99+
- **Sensitive parameters**: `password`, `token`, `api_key`, `secret`, `auth`, etc.
100+
101+
### Custom URL Sanitization
102+
103+
You can provide your own sanitization logic:
104+
105+
```ts
106+
const customSanitizer = (url: string) => {
107+
// Remove all query parameters
108+
return url.split('?')[0];
109+
};
110+
111+
// Or more targeted sanitization
112+
const targetedSanitizer = (url: string) => {
113+
return url.replace(/sessionId=[^&]*/gi, 'sessionId=REDACTED');
114+
};
115+
116+
registerInstrumentations({
117+
instrumentations: [
118+
new BrowserNavigationInstrumentation({
119+
sanitizeUrl: customSanitizer,
120+
}),
121+
],
122+
});
123+
```
124+
125+
### No Sanitization
126+
127+
If you want to record URLs without any sanitization (not recommended for production):
128+
129+
```ts
130+
registerInstrumentations({
131+
instrumentations: [
132+
new BrowserNavigationInstrumentation({
133+
// No sanitizeUrl provided - URLs recorded as-is
134+
}),
135+
],
136+
});
137+
```
138+
76139
## Adding Custom Attributes
77140

78141
If you need to add custom attributes to each navigation event, provide a callback via `applyCustomLogRecordData`:

packages/instrumentation-browser-navigation/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
export { BrowserNavigationInstrumentation } from './instrumentation';
1818
export type { BrowserNavigationInstrumentationConfig } from './types';
19+
export { defaultSanitizeUrl } from './utils';

packages/instrumentation-browser-navigation/src/instrumentation.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
ApplyCustomLogRecordDataFunction,
2525
SanitizeUrlFunction,
2626
} from './types';
27-
import { isHashChange, defaultSanitizeUrl } from './utils';
27+
import { isHashChange } from './utils';
2828

2929
/**
3030
* This class represents a browser navigation instrumentation plugin
@@ -39,7 +39,7 @@ export const ATTR_BROWSER_NAVIGATION_TYPE = 'browser.navigation.type';
3939
export class BrowserNavigationInstrumentation extends InstrumentationBase<BrowserNavigationInstrumentationConfig> {
4040
applyCustomLogRecordData: ApplyCustomLogRecordDataFunction | undefined =
4141
undefined;
42-
sanitizeUrl: SanitizeUrlFunction = defaultSanitizeUrl; // Initialize with default
42+
sanitizeUrl?: SanitizeUrlFunction; // No default sanitization
4343
private _onLoadHandler?: () => void;
4444
private _onPopStateHandler?: (ev: PopStateEvent) => void;
4545
private _onNavigateHandler?: (ev: Event) => void;
@@ -53,10 +53,7 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
5353
constructor(config: BrowserNavigationInstrumentationConfig) {
5454
super(PACKAGE_NAME, PACKAGE_VERSION, config);
5555
this.applyCustomLogRecordData = config?.applyCustomLogRecordData;
56-
// Override default with custom sanitizer if provided
57-
if (config?.sanitizeUrl) {
58-
this.sanitizeUrl = config.sanitizeUrl;
59-
}
56+
this.sanitizeUrl = config?.sanitizeUrl;
6057
}
6158

6259
init() {}
@@ -68,7 +65,7 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
6865
const navLogRecord: LogRecord = {
6966
eventName: EVENT_NAME,
7067
attributes: {
71-
[ATTR_URL_FULL]: this.sanitizeUrl(document.documentURI),
68+
[ATTR_URL_FULL]: this.sanitizeUrl ? this.sanitizeUrl(document.documentURI) : document.documentURI,
7269
[ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT]: false,
7370
[ATTR_BROWSER_NAVIGATION_HASH_CHANGE]: false,
7471
},
@@ -110,7 +107,7 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
110107
const navLogRecord: LogRecord = {
111108
eventName: EVENT_NAME,
112109
attributes: {
113-
[ATTR_URL_FULL]: this.sanitizeUrl(currentUrl),
110+
[ATTR_URL_FULL]: this.sanitizeUrl ? this.sanitizeUrl(currentUrl) : currentUrl,
114111
[ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT]: sameDocument,
115112
[ATTR_BROWSER_NAVIGATION_HASH_CHANGE]: hashChange,
116113
...(navType ? { [ATTR_BROWSER_NAVIGATION_TYPE]: navType } : {}),

packages/instrumentation-browser-navigation/test/navigation.test.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323

2424
import * as sinon from 'sinon';
2525
import { BrowserNavigationInstrumentation } from '../src';
26+
import { defaultSanitizeUrl } from '../src/utils';
2627
import {
2728
EVENT_NAME,
2829
ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT,
@@ -373,7 +374,7 @@ describe('Browser Navigation Instrumentation', () => {
373374
);
374375
window.removeEventListener('popstate', popstateHandler);
375376
done();
376-
}, 50);
377+
}, 150);
377378
};
378379

379380
window.addEventListener('popstate', popstateHandler);
@@ -419,7 +420,7 @@ describe('Browser Navigation Instrumentation', () => {
419420
}
420421
};
421422

422-
entryChangeHandler = (event: any) => {
423+
entryChangeHandler = (_event: any) => {
423424
// Let the navigation complete, then check records
424425
setTimeout(() => {
425426
const records = exporter.getFinishedLogRecords();
@@ -470,7 +471,7 @@ describe('Browser Navigation Instrumentation', () => {
470471
completeDone();
471472
}
472473
}, 50);
473-
} catch (_error) {
474+
} catch {
474475
// Fallback if Navigation API methods fail
475476
console.log(
476477
'Navigation API methods not fully supported, using fallback'
@@ -502,12 +503,20 @@ describe('Browser Navigation Instrumentation', () => {
502503
});
503504

504505
it('should export LogRecord with Navigation API hash change detection', done => {
506+
let testCompleted = false;
507+
const completeDone = (error?: Error) => {
508+
if (!testCompleted) {
509+
testCompleted = true;
510+
done(error);
511+
}
512+
};
513+
505514
// Check if Navigation API is actually available in the test environment
506515
if (!(window as any).navigation) {
507516
console.log(
508517
'Navigation API not available in test environment, skipping test'
509518
);
510-
done();
519+
completeDone();
511520
return;
512521
}
513522

@@ -530,7 +539,7 @@ describe('Browser Navigation Instrumentation', () => {
530539
}
531540
};
532541

533-
entryChangeHandler = (event: any) => {
542+
entryChangeHandler = (_event: any) => {
534543
setTimeout(() => {
535544
const records = exporter.getFinishedLogRecords();
536545
if (records.length >= 1) {
@@ -553,7 +562,7 @@ describe('Browser Navigation Instrumentation', () => {
553562
];
554563
assert.ok(navType === 'push' || navType === 'traverse');
555564
cleanup();
556-
done();
565+
completeDone();
557566
}
558567
}, 10);
559568
};
@@ -570,18 +579,18 @@ describe('Browser Navigation Instrumentation', () => {
570579
const navLogRecord = records.slice(-1)[0] as ReadableLogRecord;
571580
assert.strictEqual(navLogRecord.eventName, EVENT_NAME);
572581
cleanup();
573-
done();
582+
completeDone();
574583
} else {
575584
console.log('No hash navigation records found');
576585
cleanup();
577-
done();
586+
completeDone();
578587
}
579588
}, 100);
580589

581590
// Cleanup timeout
582591
setTimeout(() => {
583592
cleanup();
584-
done(new Error('Test timeout - Hash change navigation not detected'));
593+
completeDone(new Error('Test timeout - Hash change navigation not detected'));
585594
}, 1000);
586595
}, 10); // Close the setTimeout for readyState wait
587596
});
@@ -590,6 +599,7 @@ describe('Browser Navigation Instrumentation', () => {
590599
instrumentation = new BrowserNavigationInstrumentation({
591600
enabled: true,
592601
useNavigationApiIfAvailable: false, // Test history API path
602+
sanitizeUrl: defaultSanitizeUrl,
593603
});
594604
instrumentation.enable();
595605

@@ -664,7 +674,7 @@ describe('Browser Navigation Instrumentation', () => {
664674
'Should preserve normal query params'
665675
);
666676
done();
667-
}, 10);
677+
}, 150);
668678
});
669679

670680
it('should work with Navigation API enabled', done => {

0 commit comments

Comments
 (0)