plugins/browser-plugin-error-tracking/src/index.ts
/*
* Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import {
isFunction,
addEventListener,
BrowserPlugin,
BrowserTracker,
dispatchToTrackersInCollection,
} from '@snowplow/browser-tracker-core';
import { buildSelfDescribingEvent, CommonEventProperties, SelfDescribingJson } from '@snowplow/tracker-core';
import { truncateString } from './util';
let _trackers: Record<string, BrowserTracker> = {};
export function ErrorTrackingPlugin(): BrowserPlugin {
return {
activateBrowserPlugin: (tracker: BrowserTracker) => {
_trackers[tracker.id] = tracker;
},
};
}
/**
* Event for tracking an error
*/
export interface ErrorEventProperties {
/** The error message */
message: string;
/** The filename where the error occurred */
filename?: string;
/** The line number which the error occurred on */
lineno?: number;
/** The column number which the error occurred on */
colno?: number;
/** The error object */
error?: Error;
}
/**
* Send error as self-describing event
*
* @param event - The event information
* @param trackers - The tracker identifiers which the event will be sent to
*/
export function trackError(
event: ErrorEventProperties & CommonEventProperties,
trackers: Array<string> = Object.keys(_trackers)
) {
const { message, filename, lineno, colno, error, context, timestamp } = event,
stack = error && truncateString(error.stack, 8192),
truncatedMessage = message && truncateString(message, 2048);
dispatchToTrackersInCollection(trackers, _trackers, (t) => {
t.core.track(
buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplowanalytics.snowplow/application_error/jsonschema/1-0-1',
data: {
programmingLanguage: 'JAVASCRIPT',
message: truncatedMessage ?? 'trackError called without required message',
stackTrace: stack,
lineNumber: lineno,
lineColumn: colno,
fileName: filename,
},
},
}),
context,
timestamp
);
});
}
/**
* The configuration for automatic error tracking
*/
export interface ErrorTrackingConfiguration {
/** A callback which allows only certain errors to be tracked */
filter?: (error: ErrorEvent | Event) => boolean;
/** A callback to dynamically add extra context based on the error */
contextAdder?: (error: ErrorEvent | Event) => Array<SelfDescribingJson>;
/** Context to be added to every error */
context?: Array<SelfDescribingJson>;
}
/**
* Enable automatic error tracking, added event handler for 'error' event on window
* @param configuration - The error tracking configuration
* @param trackers - The tracker identifiers which the event will be sent to
*/
export function enableErrorTracking(
configuration: ErrorTrackingConfiguration = {},
trackers: Array<string> = Object.keys(_trackers)
) {
const { filter, contextAdder, context } = configuration,
captureError = (errorEvent: ErrorEvent | Event) => {
if ((filter && isFunction(filter) && filter(errorEvent)) || filter == null) {
sendError({ errorEvent: errorEvent, commonContext: context, contextAdder }, trackers);
}
};
addEventListener(window, 'error', captureError, true);
}
function sendError(
{
errorEvent,
commonContext,
contextAdder,
}: {
errorEvent: ErrorEvent | Event;
commonContext?: Array<SelfDescribingJson>;
contextAdder?: (error: ErrorEvent | Event) => Array<SelfDescribingJson>;
},
trackers: Array<string>
) {
let context = commonContext || [];
if (contextAdder && isFunction(contextAdder)) {
context = context.concat(contextAdder(errorEvent));
}
if ('message' in errorEvent) {
trackError(
{
message: errorEvent.message,
filename: errorEvent.filename,
lineno: errorEvent.lineno,
colno: errorEvent.colno,
error: errorEvent.error,
context,
},
trackers
);
} else if (errorEvent.target && 'tagName' in errorEvent.target) {
const element: any = errorEvent.target;
trackError(
{
message: `Non-script error on ${element.tagName} element`,
filename: element.src || undefined,
context,
},
trackers
);
} else {
trackError(
{
message: "JS Exception. Browser doesn't support ErrorEvent API",
context,
},
trackers
);
}
}