feat(API): Set up error tracking using Sentry (#4394)

* feat(cli): Setup error tracking using Sentry

* make error reporting available in the workflows package

* address some of the PR comments

* create a ErrorReporterProxy like LoggerProxy

* remove the `captureError` helper. use ErrorReporterProxy directly

* fix linting issues

* remove ErrorReporterProxy warnings in tests

* check for NODE_ENV === 'production' instead

* IErrorReporter -> ErrorReporter

* ErrorReporterProxy.getInstance() -> ErrorReporter

* allow capturing stacks in warnings as well

* make n8n debugging consistent with `npm start`

* IReportingOptions -> ReportingOptions

* use consistent signature for `error` and `warn`

* use Logger instead of console.log
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2022-11-04 17:34:47 +01:00 committed by GitHub
parent 0edd4bcc87
commit 41cb0eec6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 15097 additions and 8015 deletions

1
.vscode/launch.json vendored
View file

@ -14,6 +14,7 @@
{
"name": "Launch n8n with debug",
"program": "${workspaceFolder}/packages/cli/bin/n8n",
"cwd": "${workspaceFolder}/packages/cli/bin",
"request": "launch",
"skipFiles": ["<node_internals>/**"],
"type": "node",

View file

@ -4,6 +4,7 @@ ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
ENV N8N_VERSION=${N8N_VERSION}
RUN \
apt-get update && \
apt-get -y install graphicsmagick gosu git

View file

@ -4,6 +4,7 @@ ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
ENV N8N_VERSION=${N8N_VERSION}
RUN \
yum install -y gcc-c++ make

View file

@ -4,6 +4,7 @@ FROM n8nio/base:${NODE_VERSION}
ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
ENV N8N_VERSION=${N8N_VERSION}
ENV NODE_ENV=production
RUN set -eux; \
apkArch="$(apk --print-arch)"; \

23169
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -948,6 +948,15 @@ export const schema = {
env: 'N8N_DIAGNOSTICS_POSTHOG_DISABLE_RECORDING',
},
},
sentry: {
dsn: {
doc: 'Data source name for error tracking on Sentry',
format: String,
default:
'https://1f954e089a054b8e943ae4f4042b2bff@o1420875.ingest.sentry.io/4504016528408576',
env: 'N8N_SENTRY_DSN',
},
},
frontend: {
doc: 'Diagnostics config for frontend.',
format: String,

View file

@ -104,6 +104,8 @@
"@oclif/core": "^1.9.3",
"@oclif/errors": "^1.2.2",
"@rudderstack/rudder-sdk-node": "1.0.6",
"@sentry/node": "^7.17.3",
"@sentry/integrations": "^7.17.3",
"axios": "^0.21.1",
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",

View file

@ -32,6 +32,7 @@ import {
WorkflowActivationError,
WorkflowExecuteMode,
LoggerProxy as Logger,
ErrorReporterProxy as ErrorReporter,
} from 'n8n-workflow';
import express from 'express';
@ -121,6 +122,7 @@ export class ActiveWorkflowRunner {
});
console.log(` => Started`);
} catch (error) {
ErrorReporter.error(error);
console.log(
` => ERROR: Workflow could not be activated on first try, keep on trying`,
);
@ -881,6 +883,7 @@ export class ActiveWorkflowRunner {
try {
await this.add(workflowId, activationMode, workflowData);
} catch (error) {
ErrorReporter.error(error);
let lastTimeout = this.queuedWorkflowActivations[workflowId].lastTimeout;
if (lastTimeout < WORKFLOW_REACTIVATE_MAX_TIMEOUT) {
lastTimeout = Math.min(lastTimeout * 2, WORKFLOW_REACTIVATE_MAX_TIMEOUT);
@ -948,6 +951,7 @@ export class ActiveWorkflowRunner {
try {
await this.removeWorkflowWebhooks(workflowId);
} catch (error) {
ErrorReporter.error(error);
console.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`,

View file

@ -76,7 +76,7 @@ export const executeCommand = async (
command: string,
options?: { doNotHandleError?: boolean },
): Promise<string> => {
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
const execOptions = {
cwd: downloadFolder,

View file

@ -38,6 +38,7 @@ import {
WorkflowExecuteMode,
ITaskDataConnections,
LoggerProxy as Logger,
ErrorReporterProxy as ErrorReporter,
IHttpRequestHelper,
} from 'n8n-workflow';
@ -672,6 +673,7 @@ export class CredentialsHelper extends ICredentialsHelper {
credentialsDecrypted,
);
} catch (error) {
ErrorReporter.error(error);
// Do not fail any requests to allow custom error messages and
// make logic easier
if (error.cause?.response) {

View file

@ -0,0 +1,41 @@
import * as Sentry from '@sentry/node';
import { RewriteFrames } from '@sentry/integrations';
import type { Application } from 'express';
import config from '../config';
import { ErrorReporterProxy } from 'n8n-workflow';
let initialized = false;
export const initErrorHandling = (app?: Application) => {
if (initialized) return;
if (!config.getEnv('diagnostics.enabled')) {
initialized = true;
return;
}
const dsn = config.getEnv('diagnostics.config.sentry.dsn');
const { N8N_VERSION: release, ENVIRONMENT: environment } = process.env;
Sentry.init({
dsn,
release,
environment,
integrations: (integrations) => {
integrations.push(new RewriteFrames({ root: process.cwd() }));
return integrations;
},
});
if (app) {
const { requestHandler, errorHandler } = Sentry.Handlers;
app.use(requestHandler());
app.use(errorHandler());
}
ErrorReporterProxy.init({
report: (error, options) => Sentry.captureException(error, options),
});
initialized = true;
};

View file

@ -22,6 +22,7 @@ import {
IVersionedNodeType,
LoggerProxy,
jsonParse,
ErrorReporterProxy as ErrorReporter,
} from 'n8n-workflow';
import {
@ -125,19 +126,23 @@ class LoadNodesAndCredentialsClass {
const nodePackages = [];
try {
// Read downloaded nodes and credentials
const downloadedNodesFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
const downloadedNodesFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
const downloadedNodesFolderModules = path.join(downloadedNodesFolder, 'node_modules');
await fsAccess(downloadedNodesFolderModules);
const downloadedPackages = await this.getN8nNodePackages(downloadedNodesFolderModules);
nodePackages.push(...downloadedPackages);
// eslint-disable-next-line no-empty
} catch (error) {}
} catch (error) {
// Folder does not exist so ignore and return
return;
}
for (const packagePath of nodePackages) {
try {
await this.loadDataFromPackage(packagePath);
// eslint-disable-next-line no-empty
} catch (error) {}
} catch (error) {
ErrorReporter.error(error);
}
}
}
@ -231,7 +236,7 @@ class LoadNodesAndCredentialsClass {
}
async loadNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
const command = `npm install ${packageName}${version ? `@${version}` : ''}`;
await executeCommand(command);
@ -285,7 +290,7 @@ class LoadNodesAndCredentialsClass {
packageName: string,
installedPackage: InstalledPackages,
): Promise<InstalledPackages> {
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
const command = `npm i ${packageName}@latest`;

View file

@ -6,6 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Request, Response } from 'express';
import { parse, stringify } from 'flatted';
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
@ -154,9 +155,13 @@ export function send<T, R extends Request, S extends Response>(
sendSuccessResponse(res, data, raw);
} catch (error) {
if (error instanceof Error && isUniqueConstraintError(error)) {
if (error instanceof Error) {
ErrorReporter.error(error);
if (isUniqueConstraintError(error)) {
error.message = 'There is already an entry with this name';
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
sendErrorResponse(res, error);

View file

@ -70,6 +70,7 @@ import {
jsonParse,
WebhookHttpMethod,
WorkflowExecuteMode,
ErrorReporterProxy as ErrorReporter,
} from 'n8n-workflow';
import basicAuth from 'basic-auth';
@ -82,6 +83,7 @@ import parseUrl from 'parseurl';
import promClient, { Registry } from 'prom-client';
import history from 'connect-history-api-fallback';
import bodyParser from 'body-parser';
import config from '../config';
import * as Queue from './Queue';
@ -153,6 +155,7 @@ import glob from 'fast-glob';
import { ResponseError } from './ResponseHelper';
import { toHttpNodeParameters } from './CurlConverterHelper';
import { initErrorHandling } from './ErrorReporting';
require('body-parser-xml')(bodyParser);
@ -256,6 +259,8 @@ class App {
this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
initErrorHandling(this.app);
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
const telemetrySettings: ITelemetrySettings = {
enabled: config.getEnv('diagnostics.enabled'),
@ -742,6 +747,7 @@ class App {
// DB ping
await connection.query('SELECT 1');
} catch (err) {
ErrorReporter.error(err);
LoggerProxy.error('No Database connection!', err);
const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503);
return ResponseHelper.sendErrorResponse(res, error);

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { createTransport, Transporter } from 'nodemailer';
import { LoggerProxy as Logger } from 'n8n-workflow';
import { ErrorReporterProxy as ErrorReporter, LoggerProxy as Logger } from 'n8n-workflow';
import * as config from '../../../config';
import { MailData, SendEmailResult, UserManagementMailerImplementation } from './Interfaces';
@ -24,22 +24,15 @@ export class NodeMailer implements UserManagementMailerImplementation {
const user = config.getEnv('userManagement.emails.smtp.auth.user');
const pass = config.getEnv('userManagement.emails.smtp.auth.pass');
return new Promise((resolve, reject) => {
this.transport.verify((error: Error) => {
if (!error) {
resolve();
return;
}
const message = [];
try {
await this.transport.verify();
} catch (error) {
const message: string[] = [];
if (!host) message.push('SMTP host not defined (N8N_SMTP_HOST).');
if (!user) message.push('SMTP user not defined (N8N_SMTP_USER).');
if (!pass) message.push('SMTP pass not defined (N8N_SMTP_PASS).');
reject(new Error(message.length ? message.join(' ') : error.message));
});
});
throw message.length ? new Error(message.join(' '), { cause: error }) : error;
}
}
async sendMail(mailData: MailData): Promise<SendEmailResult> {
@ -62,6 +55,7 @@ export class NodeMailer implements UserManagementMailerImplementation {
`Email sent successfully to the following recipients: ${mailData.emailRecipients.toString()}`,
);
} catch (error) {
ErrorReporter.error(error);
Logger.error('Failed to send email', { recipients: mailData.emailRecipients, error });
return {
success: false,

View file

@ -1,7 +1,7 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable import/no-cycle */
import { Response } from 'express';
import { LoggerProxy as Logger } from 'n8n-workflow';
import { ErrorReporterProxy as ErrorReporter, LoggerProxy as Logger } from 'n8n-workflow';
import { In } from 'typeorm';
import validator from 'validator';
@ -159,6 +159,7 @@ export function usersNamespace(this: N8nApp): void {
public_api: false,
});
} catch (error) {
ErrorReporter.error(error);
Logger.error('Failed to create user shells', { userShells: createUsers });
throw new ResponseHelper.ResponseError('An error occurred during user creation');
}

View file

@ -6,8 +6,11 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-floating-promises */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { IRun, LoggerProxy as Logger, WorkflowOperationError } from 'n8n-workflow';
import {
ErrorReporterProxy as ErrorReporter,
LoggerProxy as Logger,
WorkflowOperationError,
} from 'n8n-workflow';
import { FindManyOptions, LessThanOrEqual, ObjectLiteral } from 'typeorm';
import { DateUtils } from 'typeorm/util/DateUtils';
@ -20,8 +23,6 @@ import {
IExecutionsStopData,
IWorkflowExecutionDataProcess,
ResponseHelper,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowRunner,
} from '.';
import { getWorkflowOwner } from './UserManagement/UserManagementHelper';
@ -173,7 +174,8 @@ export class WaitTrackerClass {
// Start the execution again
const workflowRunner = new WorkflowRunner();
await workflowRunner.run(data, false, false, executionId);
})().catch((error) => {
})().catch((error: Error) => {
ErrorReporter.error(error);
Logger.error(
`There was a problem starting the waiting execution with id "${executionId}": "${error.message}"`,
{ executionId },

View file

@ -34,6 +34,7 @@ import {
IWebhookResponseData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowExecuteAdditionalData,
ErrorReporterProxy as ErrorReporter,
LoggerProxy as Logger,
NodeHelpers,
Workflow,
@ -434,6 +435,7 @@ export async function executeWebhook(
didSendResponse = true;
})
.catch(async (error) => {
ErrorReporter.error(error);
Logger.error(
`Error with Webhook-Response for execution "${executionId}": "${error.message}"`,
{ executionId, workflowId: workflow.id },

View file

@ -33,6 +33,7 @@ import {
import config from '../config';
// eslint-disable-next-line import/no-cycle
import { WEBHOOK_METHODS } from './WebhookHelpers';
import { initErrorHandling } from './ErrorReporting';
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call
require('body-parser-xml')(bodyParser);
@ -217,6 +218,8 @@ class App {
this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
initErrorHandling(this.app);
}
/**

View file

@ -32,6 +32,7 @@ import {
IWorkflowExecuteHooks,
IWorkflowHooksOptionalParameters,
IWorkflowSettings,
ErrorReporterProxy as ErrorReporter,
LoggerProxy as Logger,
SubworkflowOperationError,
Workflow,
@ -162,7 +163,8 @@ export function executeErrorWorkflow(
user,
);
})
.catch((error) => {
.catch((error: Error) => {
ErrorReporter.error(error);
Logger.error(
`Could not execute ErrorWorkflow for execution ID ${this.executionId} because of error querying the workflow owner`,
{
@ -219,8 +221,9 @@ function pruneExecutionData(this: WorkflowHooks): void {
}, timeout * 1000),
)
.catch((error) => {
throttling = false;
ErrorReporter.error(error);
throttling = false;
Logger.error(
`Failed pruning execution data from database for execution ID ${this.executionId} (hookFunctionsSave)`,
{
@ -451,6 +454,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
flattenedExecutionData as IExecutionFlattedDb,
);
} catch (err) {
ErrorReporter.error(err);
// TODO: Improve in the future!
// Errors here might happen because of database access
// For busy machines, we may get "Database is locked" errors.
@ -511,6 +515,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
newStaticData,
);
} catch (e) {
ErrorReporter.error(e);
Logger.error(
`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (hookFunctionsSave)`,
{ executionId: this.executionId, workflowId: this.workflowData.id },
@ -629,6 +634,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
);
}
} catch (error) {
ErrorReporter.error(error);
Logger.error(`Failed saving execution data to DB on execution ID ${this.executionId}`, {
executionId: this.executionId,
workflowId: this.workflowData.id,
@ -676,6 +682,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
newStaticData,
);
} catch (e) {
ErrorReporter.error(e);
Logger.error(
`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (workflowExecuteAfter)`,
{ sessionId: this.sessionId, workflowId: this.workflowData.id },

View file

@ -18,6 +18,7 @@ import {
IRun,
IRunExecutionData,
ITaskData,
ErrorReporterProxy as ErrorReporter,
LoggerProxy as Logger,
NodeApiError,
NodeOperationError,
@ -232,6 +233,7 @@ export async function executeErrorWorkflow(
const workflowRunner = new WorkflowRunner();
await workflowRunner.run(runData);
} catch (error) {
ErrorReporter.error(error);
Logger.error(
`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`,
{ workflowId: workflowErrorData.workflow.id },
@ -407,9 +409,10 @@ export async function saveStaticData(workflow: Workflow): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
await saveStaticDataById(workflow.id!, workflow.staticData);
workflow.staticData.__dataChanged = false;
} catch (e) {
} catch (error) {
ErrorReporter.error(error);
Logger.error(
`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`,
`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${error.message}"`,
{ workflowId: workflow.id },
);
}

View file

@ -15,6 +15,7 @@
import { BinaryDataManager, IProcessMessage, WorkflowExecute } from 'n8n-core';
import {
ErrorReporterProxy as ErrorReporter,
ExecutionError,
IDeferredPromise,
IExecuteResponsePromiseData,
@ -56,6 +57,7 @@ import * as Queue from './Queue';
import { InternalHooksManager } from './InternalHooksManager';
import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper';
import { generateFailedExecutionFromError } from './WorkflowHelpers';
import { initErrorHandling } from './ErrorReporting';
export class WorkflowRunner {
activeExecutions: ActiveExecutions.ActiveExecutions;
@ -76,6 +78,8 @@ export class WorkflowRunner {
if (executionsMode === 'queue') {
this.jobQueue = Queue.getInstance().getBullObjectInstance();
}
initErrorHandling();
}
/**
@ -98,6 +102,8 @@ export class WorkflowRunner {
executionId: string,
hooks?: WorkflowHooks,
) {
ErrorReporter.error(error);
const fullRunData: IRun = {
data: {
resultData: {
@ -169,6 +175,7 @@ export class WorkflowRunner {
);
})
.catch((error) => {
ErrorReporter.error(error);
console.error('There was a problem running internal hook "onWorkflowPostExecute"', error);
});
@ -182,6 +189,7 @@ export class WorkflowRunner {
]);
})
.catch((error) => {
ErrorReporter.error(error);
console.error('There was a problem running hook "workflow.postExecute"', error);
});
}
@ -263,6 +271,7 @@ export class WorkflowRunner {
try {
await checkPermissionsForExecution(workflow, data.userId);
} catch (error) {
ErrorReporter.error(error);
// Create a failed execution with the data for the node
// save it and abort execution
const failedExecution = generateFailedExecutionFromError(
@ -503,6 +512,7 @@ export class WorkflowRunner {
clearWatchdogInterval();
}
} catch (error) {
ErrorReporter.error(error);
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
// "workflowExecuteAfter" which we require.
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(

View file

@ -9,6 +9,7 @@
import { BinaryDataManager, IProcessMessage, UserSettings, WorkflowExecute } from 'n8n-core';
import {
ErrorReporterProxy as ErrorReporter,
ExecutionError,
ICredentialType,
ICredentialTypeData,
@ -52,6 +53,7 @@ import { InternalHooksManager } from './InternalHooksManager';
import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper';
import { loadClassInIsolation } from './CommunityNodes/helpers';
import { generateFailedExecutionFromError } from './WorkflowHelpers';
import { initErrorHandling } from './ErrorReporting';
export class WorkflowRunnerProcess {
data: IWorkflowExecutionDataProcessWithExecution | undefined;
@ -79,6 +81,10 @@ export class WorkflowRunnerProcess {
}, 30000);
}
constructor() {
initErrorHandling();
}
async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> {
process.on('SIGTERM', WorkflowRunnerProcess.stopProcess);
process.on('SIGINT', WorkflowRunnerProcess.stopProcess);
@ -265,6 +271,7 @@ export class WorkflowRunnerProcess {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
await sendToParentProcess('sendMessageToUI', { source, message });
} catch (error) {
ErrorReporter.error(error);
this.logger.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
`There was a problem sending UI data to parent process: "${error.message}"`,
@ -402,6 +409,7 @@ export class WorkflowRunnerProcess {
parameters,
});
} catch (error) {
ErrorReporter.error(error);
this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error });
}
}

View file

@ -256,7 +256,7 @@ export function getUserN8nFolderCustomExtensionPath(): string {
* have been downloaded
*
*/
export function getUserN8nFolderDowloadedNodesPath(): string {
export function getUserN8nFolderDownloadedNodesPath(): string {
return path.join(getUserN8nFolderPath(), DOWNLOADED_NODES_SUBDIRECTORY);
}

View file

@ -0,0 +1,36 @@
import type { Primitives } from './utils';
import * as Logger from './LoggerProxy';
export interface ReportingOptions {
level?: 'warning' | 'error';
tags?: Record<string, Primitives>;
extra?: Record<string, unknown>;
}
interface ErrorReporter {
report: (error: Error | string, options?: ReportingOptions) => void;
}
const isProduction = process.env.NODE_ENV === 'production';
const instance: ErrorReporter = {
report: (error, options) => isProduction && Logger.error('ERROR', { error, options }),
};
export function init(errorReporter: ErrorReporter) {
instance.report = errorReporter.report;
}
const wrap = (e: unknown) => {
if (e instanceof Error) return e;
if (typeof e === 'string') return new Error(e);
return;
};
export const error = (e: unknown, options?: ReportingOptions) => {
const toReport = wrap(e);
if (toReport) instance.report(toReport, options);
};
export const warn = (warning: Error | string, options?: ReportingOptions) =>
error(warning, { level: 'warning', ...options });

View file

@ -1,4 +1,5 @@
import * as LoggerProxy from './LoggerProxy';
export * as ErrorReporterProxy from './ErrorReporterProxy';
import * as NodeHelpers from './NodeHelpers';
import * as ObservableObject from './ObservableObject';
import * as TelemetryHelpers from './TelemetryHelpers';

View file

@ -1,5 +1,8 @@
import * as ErrorReporter from './ErrorReporterProxy';
export type Primitives = string | number | boolean | bigint | symbol | null | undefined;
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */
type Primitives = string | number | boolean | bigint | symbol | null | undefined;
export const deepCopy = <T extends ((object | Date) & { toJSON?: () => string }) | Primitives>(
source: T,
hash = new WeakMap(),
@ -16,6 +19,9 @@ export const deepCopy = <T extends ((object | Date) & { toJSON?: () => string })
return source.toJSON() as T;
}
if (hash.has(source)) {
ErrorReporter.warn('Circular reference detected', {
extra: { source, path },
});
return hash.get(source);
}
// Array