Skip to content

Commit f8da512

Browse files
committed
suggested changes
1 parent 6fc6a26 commit f8da512

File tree

7 files changed

+625
-336
lines changed

7 files changed

+625
-336
lines changed

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/instrumentation-browser-navigation/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
"karma-jquery": "0.2.4"
5656
},
5757
"dependencies": {
58-
"@opentelemetry/instrumentation": "^0.208.0"
58+
"@opentelemetry/instrumentation": "^0.208.0",
59+
"@opentelemetry/semantic-conventions": "^1.27.0"
5960
},
6061
"homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-browser-navigation#readme"
6162
}

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

Lines changed: 74 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,20 @@
1616

1717
import { InstrumentationBase, isWrapped } from '@opentelemetry/instrumentation';
1818
import { LogRecord } from '@opentelemetry/api-logs';
19+
import { ATTR_URL_FULL } from '@opentelemetry/semantic-conventions';
1920
import { PACKAGE_NAME, PACKAGE_VERSION } from './version';
2021
import {
2122
BrowserNavigationInstrumentationConfig,
2223
NavigationType,
2324
ApplyCustomLogRecordDataFunction,
25+
SanitizeUrlFunction,
2426
} from './types';
27+
import { isHashChange, defaultSanitizeUrl } from './utils';
2528

2629
/**
2730
* This class represents a browser navigation instrumentation plugin
2831
*/
2932
export const EVENT_NAME = 'browser.navigation';
30-
export const ATTR_URL_FULL = 'url.full';
3133
export const ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT =
3234
'browser.navigation.same_document';
3335
export const ATTR_BROWSER_NAVIGATION_HASH_CHANGE =
@@ -37,10 +39,12 @@ export const ATTR_BROWSER_NAVIGATION_HASH_TYPE = 'browser.navigation.type';
3739
export class BrowserNavigationInstrumentation extends InstrumentationBase<BrowserNavigationInstrumentationConfig> {
3840
applyCustomLogRecordData: ApplyCustomLogRecordDataFunction | undefined =
3941
undefined;
42+
sanitizeUrl: SanitizeUrlFunction = defaultSanitizeUrl; // Initialize with default
4043
private _onLoadHandler?: () => void;
4144
private _onPopStateHandler?: (ev: PopStateEvent) => void;
4245
private _onNavigateHandler?: (ev: Event) => void;
4346
private _lastUrl: string = location.href;
47+
private _hasProcessedInitialLoad: boolean = false;
4448

4549
/**
4650
*
@@ -49,6 +53,10 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
4953
constructor(config: BrowserNavigationInstrumentationConfig) {
5054
super(PACKAGE_NAME, PACKAGE_VERSION, config);
5155
this.applyCustomLogRecordData = config?.applyCustomLogRecordData;
56+
// Override default with custom sanitizer if provided
57+
if (config?.sanitizeUrl) {
58+
this.sanitizeUrl = config.sanitizeUrl;
59+
}
5260
}
5361

5462
init() {}
@@ -60,7 +68,7 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
6068
const navLogRecord: LogRecord = {
6169
eventName: EVENT_NAME,
6270
attributes: {
63-
[ATTR_URL_FULL]: this._sanitizeUrl(document.documentURI),
71+
[ATTR_URL_FULL]: this.sanitizeUrl(document.documentURI),
6472
[ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT]: false,
6573
[ATTR_BROWSER_NAVIGATION_HASH_CHANGE]: false,
6674
},
@@ -78,8 +86,8 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
7886
) {
7987
const referrerUrl = this._lastUrl;
8088
const currentUrl =
81-
changeState === 'navigate' && navigationEvent?.destination?.url
82-
? navigationEvent.destination.url
89+
changeState === 'currententrychange' && navigationEvent?.target?.currentEntry?.url
90+
? navigationEvent.target.currentEntry.url
8391
: location.href;
8492

8593
if (referrerUrl === currentUrl) {
@@ -102,7 +110,7 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
102110
const navLogRecord: LogRecord = {
103111
eventName: EVENT_NAME,
104112
attributes: {
105-
[ATTR_URL_FULL]: this._sanitizeUrl(currentUrl),
113+
[ATTR_URL_FULL]: this.sanitizeUrl(currentUrl),
106114
[ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT]: sameDocument,
107115
[ATTR_BROWSER_NAVIGATION_HASH_CHANGE]: hashChange,
108116
...(navType ? { [ATTR_BROWSER_NAVIGATION_HASH_TYPE]: navType } : {}),
@@ -119,11 +127,24 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
119127
* executes callback {_onDOMContentLoaded } when the page is viewed
120128
*/
121129
private _waitForPageLoad() {
130+
// Check if document has already loaded completely
131+
if (document.readyState === 'complete' && !this._hasProcessedInitialLoad) {
132+
this._hasProcessedInitialLoad = true;
133+
// Use setTimeout to allow tests to reset exporter before this fires
134+
setTimeout(() => this._onHardNavigation(), 0);
135+
return;
136+
}
137+
122138
// Ensure previous handler is removed before adding a new one
123139
if (this._onLoadHandler) {
124140
document.removeEventListener('DOMContentLoaded', this._onLoadHandler);
125141
}
126-
this._onLoadHandler = this._onHardNavigation.bind(this);
142+
this._onLoadHandler = () => {
143+
if (!this._hasProcessedInitialLoad) {
144+
this._hasProcessedInitialLoad = true;
145+
this._onHardNavigation();
146+
}
147+
};
127148
document.addEventListener('DOMContentLoaded', this._onLoadHandler);
128149
}
129150

@@ -147,13 +168,13 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
147168

148169
if (navigationApi) {
149170
if (this._onNavigateHandler) {
150-
navigationApi.removeEventListener('navigate', this._onNavigateHandler);
171+
navigationApi.removeEventListener('currententrychange', this._onNavigateHandler);
151172
this._onNavigateHandler = undefined;
152173
}
153174
this._onNavigateHandler = (event: any) => {
154-
this._onSoftNavigation('navigate', event);
175+
this._onSoftNavigation('currententrychange', event);
155176
};
156-
navigationApi.addEventListener('navigate', this._onNavigateHandler);
177+
navigationApi.addEventListener('currententrychange', this._onNavigateHandler);
157178
} else {
158179
if (this._onPopStateHandler) {
159180
window.removeEventListener('popstate', this._onPopStateHandler);
@@ -183,14 +204,16 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
183204
try {
184205
const navigationApi = (window as any).navigation as EventTarget;
185206
navigationApi?.removeEventListener?.(
186-
'navigate',
207+
'currententrychange',
187208
this._onNavigateHandler
188209
);
189210
} catch {
190211
// Ignore errors when removing Navigation API listeners
191212
}
192213
this._onNavigateHandler = undefined;
193214
}
215+
// Reset the initial load flag so it can be processed again if re-enabled
216+
this._hasProcessedInitialLoad = false;
194217
}
195218

196219
/**
@@ -243,53 +266,28 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
243266
}
244267
}
245268

246-
private _isHashChange(fromUrl: string, toUrl: string): boolean {
247-
try {
248-
const a = new URL(fromUrl, window.location.origin);
249-
const b = new URL(toUrl, window.location.origin);
250-
// Only consider it a hash change if:
251-
// 1. Base URL (origin + pathname + search) is identical
252-
// 2. Both URLs have hashes and they're different, OR we're adding a hash
253-
const sameBase =
254-
a.origin === b.origin &&
255-
a.pathname === b.pathname &&
256-
a.search === b.search;
257-
const fromHasHash = a.hash !== '';
258-
const toHasHash = b.hash !== '';
259-
const hashesAreDifferent = a.hash !== b.hash;
260-
261-
return (
262-
sameBase &&
263-
hashesAreDifferent &&
264-
((fromHasHash && toHasHash) || (!fromHasHash && toHasHash))
265-
);
266-
} catch {
267-
// Fallback: check if base URLs are identical and we're changing/adding hash (not removing)
268-
const fromBase = fromUrl.split('#')[0];
269-
const toBase = toUrl.split('#')[0];
270-
const fromHash = fromUrl.split('#')[1] || '';
271-
const toHash = toUrl.split('#')[1] || '';
272-
273-
const sameBase = fromBase === toBase;
274-
const hashesAreDifferent = fromHash !== toHash;
275-
const notRemovingHash = toHash !== ''; // Only true if we're not removing the hash
276-
277-
return sameBase && hashesAreDifferent && notRemovingHash;
278-
}
279-
}
280269

281270
private _determineSameDocument(
282271
changeState?: string | null,
283272
navigationEvent?: any,
284273
fromUrl?: string,
285274
toUrl?: string
286275
): boolean {
287-
// For Navigation API events, use the sameDocument property if available
288-
if (
289-
changeState === 'navigate' &&
290-
navigationEvent?.destination?.sameDocument !== undefined
291-
) {
292-
return navigationEvent.destination.sameDocument;
276+
// For Navigation API currententrychange events
277+
if (changeState === 'currententrychange') {
278+
// For currententrychange, we can check if the navigation was same-document
279+
// by comparing origins or checking if it's a SPA navigation
280+
if (fromUrl && toUrl) {
281+
try {
282+
const fromURL = new URL(fromUrl);
283+
const toURL = new URL(toUrl);
284+
return fromURL.origin === toURL.origin;
285+
} catch {
286+
return true; // Fallback to same document
287+
}
288+
}
289+
// Default to true for same-document navigations in SPAs
290+
return true;
293291
}
294292

295293
// For other navigation types, determine based on URL comparison
@@ -321,110 +319,47 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
321319
fromUrl?: string,
322320
toUrl?: string
323321
): boolean {
324-
// For Navigation API events, use the hashChange property if available
325-
if (
326-
changeState === 'navigate' &&
327-
navigationEvent?.hashChange !== undefined
328-
) {
329-
return navigationEvent.hashChange;
322+
// For Navigation API currententrychange events, determine based on URL comparison
323+
if (changeState === 'currententrychange') {
324+
if (fromUrl && toUrl) {
325+
return isHashChange(fromUrl, toUrl);
326+
}
327+
return false;
330328
}
331329

332330
// For all other cases, determine based on URL comparison
333331
if (fromUrl && toUrl) {
334-
return this._isHashChange(fromUrl, toUrl);
332+
return isHashChange(fromUrl, toUrl);
335333
}
336334

337335
return false;
338336
}
339337

340-
/**
341-
* Sanitizes URL according to OpenTelemetry specification:
342-
* - Redacts credentials (username:password)
343-
* - Redacts sensitive query parameters
344-
* - Preserves fragment when available
345-
*/
346-
private _sanitizeUrl(url: string): string {
347-
const sensitiveParams = [
348-
'password',
349-
'passwd',
350-
'secret',
351-
'api_key',
352-
'apikey',
353-
'auth',
354-
'authorization',
355-
'token',
356-
'access_token',
357-
'refresh_token',
358-
'jwt',
359-
'session',
360-
'sessionid',
361-
'key',
362-
'private_key',
363-
'client_secret',
364-
'client_id',
365-
'signature',
366-
'hash',
367-
];
368-
try {
369-
const urlObj = new URL(url);
370-
371-
// Redact credentials if present
372-
if (urlObj.username || urlObj.password) {
373-
urlObj.username = 'REDACTED';
374-
urlObj.password = 'REDACTED';
375-
}
376-
377-
// Redact sensitive query parameters
378-
379-
for (const param of sensitiveParams) {
380-
if (urlObj.searchParams.has(param)) {
381-
urlObj.searchParams.set(param, 'REDACTED');
382-
}
383-
}
384-
385-
return urlObj.toString();
386-
} catch {
387-
// If URL parsing fails, redact credentials and sensitive query parameters
388-
let sanitized = url.replace(/\/\/[^:]+:[^@]+@/, '//REDACTED:REDACTED@');
389-
390-
for (const param of sensitiveParams) {
391-
// Match param=value or param%3Dvalue (URL encoded)
392-
const regex = new RegExp(`([?&]${param}(?:%3D|=))[^&]*`, 'gi');
393-
sanitized = sanitized.replace(regex, '$1REDACTED');
394-
}
395-
396-
return sanitized;
397-
}
398-
}
399338

400339
private _mapChangeStateToType(
401340
changeState?: string | null,
402341
navigationEvent?: any
403342
): NavigationType | undefined {
404-
// For Navigation API events, check if it's a hash change first
405-
if (changeState === 'navigate' && navigationEvent?.hashChange) {
406-
// Hash changes are always considered 'push' operations semantically
407-
return 'push';
408-
}
409-
410-
// For Navigation API events, determine type based on event properties
411-
if (changeState === 'navigate') {
412-
// Check if this is a back/forward navigation (traverse)
413-
if (navigationEvent?.navigationType === 'traverse') {
414-
return 'traverse';
415-
}
416-
417-
// Check if this is a replace operation
418-
if (navigationEvent?.navigationType === 'replace') {
419-
return 'replace';
420-
}
421-
422-
// Check if this is a reload
423-
if (navigationEvent?.navigationType === 'reload') {
424-
return 'reload';
343+
// For Navigation API currententrychange events
344+
if (changeState === 'currententrychange') {
345+
// First, try to get navigation type from the currententrychange event itself
346+
if (navigationEvent?.navigationType) {
347+
const navType = navigationEvent.navigationType;
348+
switch (navType) {
349+
case 'traverse':
350+
return 'traverse';
351+
case 'replace':
352+
return 'replace';
353+
case 'reload':
354+
return 'reload';
355+
case 'push':
356+
default:
357+
return 'push';
358+
}
425359
}
426-
427-
// Default to 'push' for new navigations (link clicks, programmatic navigation)
360+
361+
// Fallback: For currententrychange events without type info, default to 'push'
362+
// Most programmatic navigations (history.pushState, link clicks) are 'push' operations
428363
return 'push';
429364
}
430365

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,16 @@ export interface BrowserNavigationInstrumentationConfig
2424
applyCustomLogRecordData?: ApplyCustomLogRecordDataFunction;
2525
/** Use the Navigation API navigate event if available (experimental) */
2626
useNavigationApiIfAvailable?: boolean;
27+
/** Custom function to sanitize URLs before adding to log records */
28+
sanitizeUrl?: SanitizeUrlFunction;
2729
}
2830

2931
export interface ApplyCustomLogRecordDataFunction {
3032
(logRecord: LogRecord): void;
3133
}
3234

35+
export interface SanitizeUrlFunction {
36+
(url: string): string;
37+
}
38+
3339
export type NavigationType = 'push' | 'replace' | 'reload' | 'traverse';

0 commit comments

Comments
 (0)