/* * 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 = {}; 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 = Object.keys(_trackers) ) { const { message, filename, lineno, colno, error, context, timestamp } = event, stack = error && truncateString(error.stack, 8192), truncatedMessage = message && truncateString(message, 2048), truncatedFilename = truncateString(filename, 1024); 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: truncatedFilename, }, }, }), 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; /** Context to be added to every error */ context?: Array; } /** * 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 = 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; contextAdder?: (error: ErrorEvent | Event) => Array; }, trackers: Array ) { 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 ); } }