Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/browser/src/BacktraceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BacktraceReport,
BacktraceRequestHandler,
BacktraceStackTraceConverter,
BreadcrumbsEventSubscriber,
DebugIdContainer,
VariableDebugIdMapProvider,
} from '@backtrace/sdk-core';
Expand All @@ -18,6 +19,7 @@ export class BacktraceClient extends BacktraceCoreClient {
handler: BacktraceRequestHandler,
attributeProviders: BacktraceAttributeProvider[],
stackTraceConverter: BacktraceStackTraceConverter,
breadcrumbsEventSubscriber: BreadcrumbsEventSubscriber[],
) {
super(
options,
Expand All @@ -27,6 +29,9 @@ export class BacktraceClient extends BacktraceCoreClient {
stackTraceConverter,
new BacktraceBrowserSessionProvider(),
new VariableDebugIdMapProvider(window as DebugIdContainer),
{
subscribers: breadcrumbsEventSubscriber,
},
);

this.captureUnhandledErrors(options.captureUnhandledErrors, options.captureUnhandledPromiseRejections);
Expand Down
170 changes: 170 additions & 0 deletions packages/browser/src/breadcrumbs/DocumentEventSubscriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
BreadcrumbLogLevel,
BreadcrumbsEventSubscriber,
BreadcrumbsManager,
BreadcrumbType,
} from '@backtrace/sdk-core';

export class DocumentEventSubscriber implements BreadcrumbsEventSubscriber {
private readonly _controller: AbortController = new AbortController();

public start(breadcrumbsManager: BreadcrumbsManager): void {
const signal = this._controller.signal;
document.addEventListener(
'click',
(mouseEvent: MouseEvent) => {
const target = mouseEvent.target as Element;
if (!target) {
return;
}

breadcrumbsManager.addBreadcrumb(
`Clicked ${target.id} ${target.tagName}`,
BreadcrumbLogLevel.Info,
BreadcrumbType.User,
{
id: target.id,
class: target.className,
name: target.tagName,
text: (target as { text?: string })?.text,
},
);
},
{
signal,
},
);

document.addEventListener(
'dblclick',
(mouseEvent: MouseEvent) => {
const target = mouseEvent.target as Element;
if (!target) {
return;
}

breadcrumbsManager.addBreadcrumb(
`Double clicked ${target.id} ${target.tagName}`,
BreadcrumbLogLevel.Info,
BreadcrumbType.User,
{
id: target.id,
class: target.className,
name: target.tagName,
text: (target as { text?: string })?.text,
},
);
},
{
signal,
},
);

document.addEventListener(
'drag',
(dragEvent: DragEvent) => {
const target = dragEvent.target as Element;
if (!target) {
return;
}

breadcrumbsManager.addBreadcrumb(
`An element ${target.id} ${target.tagName} is being dragged`,
BreadcrumbLogLevel.Debug,
BreadcrumbType.User,
{
id: target.id,
class: target.className,
name: target.tagName,
},
);
},
{
signal,
},
);

document.addEventListener(
'drop',
(dragEvent: DragEvent) => {
const target = dragEvent.target as Element;
if (!target) {
return;
}

breadcrumbsManager.addBreadcrumb(
`A dragged element is dropped on the target ${target.id} ${target.tagName}`,
BreadcrumbLogLevel.Debug,
BreadcrumbType.User,
{
id: target.id,
class: target.className,
name: target.tagName,
},
);
},
{
signal,
},
);

window.addEventListener('load', () => {
breadcrumbsManager.addBreadcrumb(`The page has loaded`, BreadcrumbLogLevel.Info, BreadcrumbType.System);
});

window.addEventListener('unload', () => {
breadcrumbsManager.addBreadcrumb(
`The page started unloading`,
BreadcrumbLogLevel.Info,
BreadcrumbType.System,
);
});

window.addEventListener('pagehide', () => {
breadcrumbsManager.addBreadcrumb(
'User navigates away from a webpage',
BreadcrumbLogLevel.Info,
BreadcrumbType.User,
);
});

window.addEventListener('pageshow', () => {
breadcrumbsManager.addBreadcrumb(
'User navigates to a webpage',
BreadcrumbLogLevel.Info,
BreadcrumbType.User,
);
});

window.addEventListener(
'online',
() => {
breadcrumbsManager.addBreadcrumb(
`The browser starts working online`,
BreadcrumbLogLevel.Info,
BreadcrumbType.System,
);
},
{
signal,
},
);

window.addEventListener(
'offline',
() => {
breadcrumbsManager.addBreadcrumb(
`The browser starts working offline `,
BreadcrumbLogLevel.Warning,
BreadcrumbType.System,
);
},
{
signal,
},
);
}
public dispose(): void {
this._controller.abort();
}
}
58 changes: 58 additions & 0 deletions packages/browser/src/breadcrumbs/HistoryEventSubscriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
BreadcrumbLogLevel,
BreadcrumbsEventSubscriber,
BreadcrumbsManager,
BreadcrumbType,
} from '@backtrace/sdk-core';

export class HistoryEventSubscriber implements BreadcrumbsEventSubscriber {
private _abortController = new AbortController();
private _originalHistoryPushStateMethod?: typeof history.pushState;
public start(breadcrumbsManager: BreadcrumbsManager): void {
if ((breadcrumbsManager.breadcrumbsType & BreadcrumbType.Navigation) !== BreadcrumbType.Navigation) {
return;
}
window.addEventListener(
'popstate',
(event: PopStateEvent) => {
breadcrumbsManager.addBreadcrumb(
`Navigating back to ${document.location}`,
BreadcrumbLogLevel.Info,
BreadcrumbType.Navigation,
{
location: document.location.toString(),
state: event.state,
},
);
},
{
signal: this._abortController.signal,
},
);

const originalHistoryPushStateMethod = history.pushState;
history.pushState = (...args) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [data, _, url] = args;
originalHistoryPushStateMethod.apply(history, args);
breadcrumbsManager.addBreadcrumb(
`Navigating to ${document.location}`,
BreadcrumbLogLevel.Info,
BreadcrumbType.Navigation,
{
url: url?.toString(),
data,
location: document.location.toString(),
},
);
};
this._originalHistoryPushStateMethod = originalHistoryPushStateMethod;
}

public dispose(): void {
this._abortController.abort();
if (this._originalHistoryPushStateMethod) {
history.pushState = this._originalHistoryPushStateMethod;
}
}
}
96 changes: 96 additions & 0 deletions packages/browser/src/breadcrumbs/WebRequestEventSubscriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
BreadcrumbLogLevel,
BreadcrumbsEventSubscriber,
BreadcrumbsManager,
BreadcrumbType,
} from '@backtrace/sdk-core';

export class WebRequestEventSubscriber implements BreadcrumbsEventSubscriber {
private _xmlHttpRequestOriginalOpenMethod?: typeof XMLHttpRequest.prototype.open;
private _fetchOriginalMethod?: typeof window.fetch;

public start(breadcrumbsManager: BreadcrumbsManager): void {
if ((breadcrumbsManager.breadcrumbsType & BreadcrumbType.Http) !== BreadcrumbType.Http) {
return;
}
const xmlHttpRequestOriginalOpenMethod = XMLHttpRequest.prototype.open;

XMLHttpRequest.prototype.open = function (
method: string,
url: string | URL,
async?: boolean,
username?: string | null,
password?: string | null,
) {
const readyStateChangeCallback = this.onreadystatechange;
this.onreadystatechange = (event: Event) => {
if (this.readyState === XMLHttpRequest.DONE) {
breadcrumbsManager.addBreadcrumb(
`Sent an HTTP ${method} request to ${url}. Response status code: ${this.status}`,
BreadcrumbLogLevel.Debug,
BreadcrumbType.Http,
{
method,
url: url.toString(),
statusCode: this.status,
},
);
}

readyStateChangeCallback?.apply(this, [event]);
};

xmlHttpRequestOriginalOpenMethod.call(this, method, url, async || true, username, password);
};

this._xmlHttpRequestOriginalOpenMethod = xmlHttpRequestOriginalOpenMethod;

const fetchOriginalMethod = window.fetch;

window.fetch = async function (resource, config) {
const method = config?.method ?? 'GET';
const attributes = {
url: resource.toString(),
method: method,
referrer: config?.referrer,
};

try {
const result = await fetchOriginalMethod(resource, config);
breadcrumbsManager.addBreadcrumb(
`Sent an HTTP ${method} request to ${resource}. Response status code: ${result.status}`,
BreadcrumbLogLevel.Debug,
BreadcrumbType.Http,
{
...attributes,
statusCode: result.status,
},
);

return result;
} catch (e) {
breadcrumbsManager.addBreadcrumb(
`HTTP ${method} failure on request to ${resource}. Reason: ${
e instanceof Error ? e.message : e?.toString() ?? 'unknown'
}`,
BreadcrumbLogLevel.Warning,
BreadcrumbType.Http,
attributes,
);
throw e;
}
};

this._fetchOriginalMethod = fetchOriginalMethod;
}

public dispose(): void {
if (this._fetchOriginalMethod) {
window.fetch = this._fetchOriginalMethod;
}

if (this._xmlHttpRequestOriginalOpenMethod) {
XMLHttpRequest.prototype.open = this._xmlHttpRequestOriginalOpenMethod;
}
}
}
10 changes: 10 additions & 0 deletions packages/browser/src/builder/BacktraceClientBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ import { WindowAttributeProvider } from '../attributes/WindowAttributeProvider';
import { BacktraceBrowserRequestHandler } from '../BacktraceBrowserRequestHandler';
import { BacktraceClient } from '../BacktraceClient';
import { BacktraceConfiguration } from '../BacktraceConfiguration';
import { DocumentEventSubscriber } from '../breadcrumbs/DocumentEventSubscriber';
import { HistoryEventSubscriber } from '../breadcrumbs/HistoryEventSubscriber';
import { WebRequestEventSubscriber } from '../breadcrumbs/WebRequestEventSubscriber';
import { JavaScriptCoreStackTraceConverter } from '../converters/JavaScriptCoreStackTraceConverter';
import { SpiderMonkeyStackTraceConverter } from '../converters/SpiderMonkeyStackTraceConverter';
import { getEngine } from '../engineDetector';

export class BacktraceClientBuilder extends BacktraceCoreClientBuilder<BacktraceClient> {
protected readonly breadcrumbsEventSubscribers = [
new WebRequestEventSubscriber(),
new DocumentEventSubscriber(),
new HistoryEventSubscriber(),
];
constructor(protected readonly options: BacktraceConfiguration) {
super(new BacktraceBrowserRequestHandler(options), [
new UserAgentAttributeProvider(),
Expand All @@ -22,12 +30,14 @@ export class BacktraceClientBuilder extends BacktraceCoreClientBuilder<Backtrace
new ApplicationInformationAttributeProvider(options),
]);
}

public build(): BacktraceClient {
return new BacktraceClient(
this.options,
this.handler,
this.attributeProviders,
this.generateStackTraceConverter(),
this.breadcrumbsEventSubscribers,
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/react/src/builder/BacktraceReactClientBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class BacktraceReactClientBuilder extends BacktraceClientBuilder {
this.handler,
this.attributeProviders,
new ReactStackTraceConverter(this.generateStackTraceConverter()),
this.breadcrumbsEventSubscribers,
);
}
}
Loading