2020-10-02 15:10:00 +00:00
|
|
|
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
|
|
|
import * as log from 'app/client/lib/log';
|
2022-10-21 10:55:01 +00:00
|
|
|
import {INotification, INotifyOptions, MessageType, Notifier} from 'app/client/models/NotifyModel';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {ApiErrorDetails} from 'app/common/ApiError';
|
|
|
|
import {fetchFromHome, pageHasHome} from 'app/common/urlUtils';
|
|
|
|
import isError = require('lodash/isError');
|
|
|
|
import pick = require('lodash/pick');
|
|
|
|
|
|
|
|
const G = getBrowserGlobals('document', 'window');
|
|
|
|
|
|
|
|
let _notifier: Notifier;
|
|
|
|
|
|
|
|
export class UserError extends Error {
|
|
|
|
public name: string = "UserError";
|
|
|
|
public key?: string;
|
|
|
|
constructor(message: string, options: {key?: string} = {}) {
|
|
|
|
super(message);
|
|
|
|
this.key = options.key;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-19 09:46:49 +00:00
|
|
|
* This error causes Notifier to show the message with an upgrade link.
|
2020-10-02 15:10:00 +00:00
|
|
|
*/
|
|
|
|
export class NeedUpgradeError extends Error {
|
|
|
|
public name: string = 'NeedUpgradeError';
|
|
|
|
constructor(message: string = 'This feature is not available in your plan') {
|
|
|
|
super(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the global Notifier instance used by subsequent reportError calls.
|
|
|
|
*/
|
|
|
|
export function setErrorNotifier(notifier: Notifier) {
|
|
|
|
_notifier = notifier;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns application errors collected by NotifyModel. Used in tests.
|
|
|
|
export function getAppErrors(): string[] {
|
|
|
|
return _notifier.getFullAppErrors().map((e) => e.error.message);
|
|
|
|
}
|
|
|
|
|
2021-10-01 19:38:58 +00:00
|
|
|
/**
|
|
|
|
* Shows normal notification without any styling or icon.
|
|
|
|
*/
|
2022-10-21 10:55:01 +00:00
|
|
|
export function reportMessage(msg: MessageType, options?: Partial<INotifyOptions>): INotification|undefined {
|
2021-10-01 19:38:58 +00:00
|
|
|
if (_notifier && !_notifier.isDisposed()) {
|
2022-10-21 10:55:01 +00:00
|
|
|
return _notifier.createUserMessage(msg, {
|
2021-10-01 19:38:58 +00:00
|
|
|
...options
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-03-29 14:40:12 +00:00
|
|
|
* Shows warning toast notification (with yellow styling), and log to server and to console. Pass
|
|
|
|
* {level: 'error'} for same behavior with adjusted styling.
|
2021-10-06 15:50:29 +00:00
|
|
|
*/
|
|
|
|
export function reportWarning(msg: string, options?: Partial<INotifyOptions>) {
|
2022-03-29 14:40:12 +00:00
|
|
|
options = {level: 'warning', ...options};
|
|
|
|
log.warn(`${options.level}: `, msg);
|
2023-04-06 15:10:29 +00:00
|
|
|
logError(msg);
|
2022-10-21 10:55:01 +00:00
|
|
|
return reportMessage(msg, options);
|
2021-10-06 15:50:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Shows success toast notification (with green styling).
|
2021-10-01 19:38:58 +00:00
|
|
|
*/
|
2022-10-21 10:55:01 +00:00
|
|
|
export function reportSuccess(msg: MessageType, options?: Partial<INotifyOptions>) {
|
|
|
|
return reportMessage(msg, {level: 'success', ...options});
|
2021-10-01 19:38:58 +00:00
|
|
|
}
|
|
|
|
|
2023-05-22 19:55:20 +00:00
|
|
|
// Errors from cross-origin scripts, and some add-ons, show up as unhelpful sanitized "Script
|
|
|
|
// error." messages. We want to know if they occur, but they are useless to the user, and useless
|
|
|
|
// to report multiple times. We report them just once to the server.
|
|
|
|
//
|
|
|
|
// In particular, this addresses a bug on iOS version of Firefox, which produces uncaught
|
|
|
|
// sanitized errors on load AND on attempts to report them, leading to a loop that hangs the
|
|
|
|
// browser. Reporting just once is a sufficient workaround.
|
|
|
|
function isUnhelpful(ev: ErrorEvent) {
|
|
|
|
return !ev.filename && !ev.lineno && ev.message?.toLowerCase().includes('script error');
|
|
|
|
}
|
|
|
|
let unhelpfulErrors = 0;
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
/**
|
|
|
|
* Report an error to the user using the global Notifier instance. If the argument is a UserError
|
|
|
|
* or an error with a status in the 400 range, it indicates a user error. Otherwise, it's an
|
|
|
|
* application error, which the user can report to us as a bug.
|
2021-10-06 15:50:29 +00:00
|
|
|
*
|
|
|
|
* Not all errors will be shown as an error toast, depending on the content of the error
|
|
|
|
* this function might show a simple toast message.
|
2020-10-02 15:10:00 +00:00
|
|
|
*/
|
2023-05-22 19:55:20 +00:00
|
|
|
export function reportError(err: Error|string, ev?: ErrorEvent): void {
|
2020-10-02 15:10:00 +00:00
|
|
|
log.error(`ERROR:`, err);
|
2021-03-01 16:51:30 +00:00
|
|
|
if (String(err).match(/GristWSConnection disposed/)) {
|
|
|
|
// This error can be emitted while a page is reloaded, and isn't worth reporting.
|
|
|
|
return;
|
|
|
|
}
|
2023-05-22 19:55:20 +00:00
|
|
|
if (ev && isUnhelpful(ev)) {
|
|
|
|
// Report just once to the server. There is little point reporting subsequent such errors once
|
|
|
|
// we know they happen, since each individual error has no useful information.
|
|
|
|
if (++unhelpfulErrors <= 1) {
|
|
|
|
logError(err);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-04-06 15:10:29 +00:00
|
|
|
logError(err);
|
2020-10-02 15:10:00 +00:00
|
|
|
if (_notifier && !_notifier.isDisposed()) {
|
|
|
|
if (!isError(err)) {
|
|
|
|
err = new Error(String(err));
|
|
|
|
}
|
|
|
|
|
|
|
|
const details: ApiErrorDetails|undefined = (err as any).details;
|
|
|
|
const code: unknown = (err as any).code;
|
|
|
|
const status: unknown = (err as any).status;
|
|
|
|
const message = (details && details.userError) || err.message;
|
|
|
|
if (details && details.limit) {
|
|
|
|
// This is a notification about reaching a plan limit. Key prevents showing multiple
|
|
|
|
// notifications for the same type of limit.
|
|
|
|
const options: Partial<INotifyOptions> = {
|
|
|
|
title: "Reached plan limit",
|
|
|
|
key: `limit:${details.limit.quantity || message}`,
|
2023-07-05 15:36:45 +00:00
|
|
|
actions: details.tips?.some(t => t.action === 'manage') ? ['manage'] : ['upgrade'],
|
2020-10-02 15:10:00 +00:00
|
|
|
};
|
|
|
|
if (details.tips && details.tips.some(tip => tip.action === 'add-members')) {
|
|
|
|
// When adding members would fix a problem, give more specific advice.
|
|
|
|
options.title = "Add users as team members first";
|
|
|
|
options.actions = [];
|
|
|
|
}
|
2021-10-06 15:50:29 +00:00
|
|
|
// Show the error as a message
|
2021-10-01 19:38:58 +00:00
|
|
|
_notifier.createUserMessage(message, options);
|
2020-10-02 15:10:00 +00:00
|
|
|
} else if (err.name === 'UserError' || (typeof status === 'number' && status >= 400 && status < 500)) {
|
|
|
|
// This is explicitly a user error, or one in the "Client Error" range, so treat it as user
|
|
|
|
// error rather than a bug. Using message as the key causes same-message notifications to
|
|
|
|
// replace previous ones rather than accumulate.
|
|
|
|
const options: Partial<INotifyOptions> = {key: (err as UserError).key || message};
|
|
|
|
if (details && details.tips && details.tips.some(tip => tip.action === 'ask-for-help')) {
|
|
|
|
options.actions = ['ask-for-help'];
|
|
|
|
}
|
2021-10-06 15:50:29 +00:00
|
|
|
_notifier.createUserMessage(message, options);
|
2020-10-02 15:10:00 +00:00
|
|
|
} else if (err.name === 'NeedUpgradeError') {
|
2021-10-06 15:50:29 +00:00
|
|
|
// Show the error as a message
|
2021-10-01 19:38:58 +00:00
|
|
|
_notifier.createUserMessage(err.message, {actions: ['upgrade'], key: 'NEED_UPGRADE'});
|
2021-01-22 14:46:27 +00:00
|
|
|
} else if (code === 'AUTH_NO_EDIT' || code === 'ACL_DENY') {
|
2021-10-06 15:50:29 +00:00
|
|
|
// Show the error as a message
|
2021-10-01 19:38:58 +00:00
|
|
|
_notifier.createUserMessage(err.message, {key: code, memos: details?.memos});
|
2022-01-25 09:38:21 +00:00
|
|
|
} else if (message.match(/\[Sandbox\].*between formula and data/)) {
|
|
|
|
// Show nicer error message for summary tables.
|
|
|
|
_notifier.createUserMessage("Summary tables can only contain formula columns.",
|
|
|
|
{key: 'summary', actions: ['ask-for-help']});
|
|
|
|
} else {
|
2020-10-02 15:10:00 +00:00
|
|
|
// If we don't recognize it, consider it an application error (bug) that the user should be
|
|
|
|
// able to report.
|
2021-11-26 10:43:55 +00:00
|
|
|
if (details?.userError) {
|
|
|
|
// If we have user friendly error, show it instead.
|
|
|
|
_notifier.createAppError(Error(details.userError));
|
|
|
|
} else {
|
|
|
|
_notifier.createAppError(err);
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set up error handlers, to report uncaught errors and rejections. These are logged to the
|
|
|
|
* console and displayed as notifications, when the notifications UI is set up.
|
|
|
|
*
|
|
|
|
* koUtil, if passed, will enable reporting errors from the evaluation of knockout computeds. It
|
|
|
|
* is passed-in as an argument to avoid creating a dependency when knockout isn't used otherwise.
|
|
|
|
*/
|
|
|
|
export function setUpErrorHandling(doReportError = reportError, koUtil?: any) {
|
|
|
|
if (koUtil) {
|
|
|
|
koUtil.setComputedErrorHandler((err: any) => doReportError(err));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Report also uncaught JS errors and unhandled Promise rejections.
|
2023-05-22 19:55:20 +00:00
|
|
|
G.window.addEventListener('error', (ev: ErrorEvent) => doReportError(ev.error || ev.message, ev));
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
G.window.addEventListener('unhandledrejection', (ev: any) => {
|
|
|
|
const reason = ev.reason || (ev.detail && ev.detail.reason);
|
|
|
|
doReportError(reason || ev);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Expose globally a function to report a notification. This is for compatibility with old UI;
|
|
|
|
// in new UI, it renders messages as user errors. New code should use `reportError()` instead.
|
|
|
|
G.window.gristNotify = (message: string) => doReportError(new UserError(message));
|
|
|
|
|
|
|
|
// Expose the function used in tests to get a list of errors in the notifier.
|
|
|
|
G.window.getAppErrors = getAppErrors;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send information about a problem to the backend. This is crude; there is some
|
|
|
|
* over-logging (regular errors such as access rights or account limits) and
|
|
|
|
* under-logging (javascript errors during startup might never get reported).
|
|
|
|
*/
|
2023-04-06 15:10:29 +00:00
|
|
|
export function logError(error: Error|string) {
|
2020-10-02 15:10:00 +00:00
|
|
|
if (!pageHasHome()) { return; }
|
2022-04-11 13:52:34 +00:00
|
|
|
const docId = G.window.gristDocPageModel?.currentDocId?.get();
|
2020-10-02 15:10:00 +00:00
|
|
|
fetchFromHome('/api/log', {
|
|
|
|
method: 'POST',
|
|
|
|
body: JSON.stringify({
|
|
|
|
// Errors don't stringify, so pick out properties explicitly for errors.
|
|
|
|
event: (error instanceof Error) ? pick(error, Object.getOwnPropertyNames(error)) : error,
|
2022-04-11 13:52:34 +00:00
|
|
|
docId,
|
2020-10-02 15:10:00 +00:00
|
|
|
page: G.window.location.href,
|
|
|
|
browser: pick(G.window.navigator, ['language', 'platform', 'userAgent'])
|
|
|
|
}),
|
|
|
|
credentials: 'include',
|
|
|
|
headers: {
|
2020-10-08 13:28:39 +00:00
|
|
|
'Content-Type': 'application/json',
|
|
|
|
'X-Requested-With': 'XMLHttpRequest',
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
}).catch(e => {
|
|
|
|
// There ... isn't much we can do about this.
|
|
|
|
// tslint:disable-next-line:no-console
|
2023-06-06 17:08:50 +00:00
|
|
|
console.warn('Failed to log event', e);
|
2020-10-02 15:10:00 +00:00
|
|
|
});
|
|
|
|
}
|