Plugins: Add monitoring only mode to frontend sandbox (#70688)

This commit is contained in:
Esteban Beltran 2023-07-05 13:48:25 +02:00 committed by GitHub
parent 549e04a8f1
commit 72f6793344
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 211 additions and 42 deletions

View File

@ -112,6 +112,7 @@ Experimental features might be changed or removed without prior notice.
| `extraThemes` | Enables extra themes | | `extraThemes` | Enables extra themes |
| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | | `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor |
| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | | `pluginsFrontendSandbox` | Enables the plugins frontend sandbox |
| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) |
| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries | | `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries |
| `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore | | `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore |
| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | | `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries |

View File

@ -98,6 +98,7 @@ export interface FeatureToggles {
extraThemes?: boolean; extraThemes?: boolean;
lokiPredefinedOperations?: boolean; lokiPredefinedOperations?: boolean;
pluginsFrontendSandbox?: boolean; pluginsFrontendSandbox?: boolean;
frontendSandboxMonitorOnly?: boolean;
sqlDatasourceDatabaseSelection?: boolean; sqlDatasourceDatabaseSelection?: boolean;
cloudWatchLogsMonacoEditor?: boolean; cloudWatchLogsMonacoEditor?: boolean;
exploreScrollableLogsContainer?: boolean; exploreScrollableLogsContainer?: boolean;

View File

@ -545,6 +545,13 @@ var (
FrontendOnly: true, FrontendOnly: true,
Owner: grafanaPluginsPlatformSquad, Owner: grafanaPluginsPlatformSquad,
}, },
{
Name: "frontendSandboxMonitorOnly",
Description: "Enables monitor only in the plugin frontend sandbox (if enabled)",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaPluginsPlatformSquad,
},
{ {
Name: "sqlDatasourceDatabaseSelection", Name: "sqlDatasourceDatabaseSelection",
Description: "Enables previous SQL data source dataset dropdown behavior", Description: "Enables previous SQL data source dataset dropdown behavior",

View File

@ -79,6 +79,7 @@ dataSourcePageHeader,preview,@grafana/enterprise-datasources,false,false,false,t
extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,false,true extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,false,true
lokiPredefinedOperations,experimental,@grafana/observability-logs,false,false,false,true lokiPredefinedOperations,experimental,@grafana/observability-logs,false,false,false,true
pluginsFrontendSandbox,experimental,@grafana/plugins-platform-backend,false,false,false,true pluginsFrontendSandbox,experimental,@grafana/plugins-platform-backend,false,false,false,true
frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,false,false,true
sqlDatasourceDatabaseSelection,preview,@grafana/grafana-bi-squad,false,false,false,true sqlDatasourceDatabaseSelection,preview,@grafana/grafana-bi-squad,false,false,false,true
cloudWatchLogsMonacoEditor,experimental,@grafana/aws-plugins,false,false,false,true cloudWatchLogsMonacoEditor,experimental,@grafana/aws-plugins,false,false,false,true
exploreScrollableLogsContainer,experimental,@grafana/observability-logs,false,false,false,true exploreScrollableLogsContainer,experimental,@grafana/observability-logs,false,false,false,true

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
79 extraThemes experimental @grafana/grafana-frontend-platform false false false true
80 lokiPredefinedOperations experimental @grafana/observability-logs false false false true
81 pluginsFrontendSandbox experimental @grafana/plugins-platform-backend false false false true
82 frontendSandboxMonitorOnly experimental @grafana/plugins-platform-backend false false false true
83 sqlDatasourceDatabaseSelection preview @grafana/grafana-bi-squad false false false true
84 cloudWatchLogsMonacoEditor experimental @grafana/aws-plugins false false false true
85 exploreScrollableLogsContainer experimental @grafana/observability-logs false false false true

View File

@ -327,6 +327,10 @@ const (
// Enables the plugins frontend sandbox // Enables the plugins frontend sandbox
FlagPluginsFrontendSandbox = "pluginsFrontendSandbox" FlagPluginsFrontendSandbox = "pluginsFrontendSandbox"
// FlagFrontendSandboxMonitorOnly
// Enables monitor only in the plugin frontend sandbox (if enabled)
FlagFrontendSandboxMonitorOnly = "frontendSandboxMonitorOnly"
// FlagSqlDatasourceDatabaseSelection // FlagSqlDatasourceDatabaseSelection
// Enables previous SQL data source dataset dropdown behavior // Enables previous SQL data source dataset dropdown behavior
FlagSqlDatasourceDatabaseSelection = "sqlDatasourceDatabaseSelection" FlagSqlDatasourceDatabaseSelection = "sqlDatasourceDatabaseSelection"

View File

@ -1,6 +1,9 @@
import { cloneDeep, isFunction } from 'lodash'; import { cloneDeep, isFunction } from 'lodash';
import { config } from '@grafana/runtime';
import { forbiddenElements } from './constants'; import { forbiddenElements } from './constants';
import { logWarning } from './utils';
/** /**
* Distortions are near-membrane mechanisms to altert JS instrics and DOM APIs. * Distortions are near-membrane mechanisms to altert JS instrics and DOM APIs.
@ -53,9 +56,11 @@ import { forbiddenElements } from './constants';
* The code in this file defines that generalDistortionMap. * The code in this file defines that generalDistortionMap.
*/ */
type DistortionMap = Map<unknown, (originalAttrOrMethod: unknown) => unknown>; type DistortionMap = Map<unknown, (originalAttrOrMethod: unknown, pluginId: string) => unknown>;
const generalDistortionMap: DistortionMap = new Map(); const generalDistortionMap: DistortionMap = new Map();
const monitorOnly = Boolean(config.featureToggles.frontendSandboxMonitorOnly);
export function getGeneralSandboxDistortionMap() { export function getGeneralSandboxDistortionMap() {
if (generalDistortionMap.size === 0) { if (generalDistortionMap.size === 0) {
// initialize the distortion map // initialize the distortion map
@ -72,7 +77,15 @@ export function getGeneralSandboxDistortionMap() {
return generalDistortionMap; return generalDistortionMap;
} }
function failToSet() { function failToSet(originalAttrOrMethod: unknown, pluginId: string) {
logWarning(`Plugin ${pluginId} tried to set a sandboxed property`, {
pluginId,
attrOrMethod: String(originalAttrOrMethod),
entity: 'window',
});
if (monitorOnly) {
return originalAttrOrMethod;
}
return () => { return () => {
throw new Error('Plugins are not allowed to set sandboxed properties'); throw new Error('Plugins are not allowed to set sandboxed properties');
}; };
@ -85,11 +98,22 @@ function distortIframeAttributes(distortions: DistortionMap) {
for (const property of iframeHtmlForbiddenProperties) { for (const property of iframeHtmlForbiddenProperties) {
const descriptor = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, property); const descriptor = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, property);
if (descriptor) { if (descriptor) {
function fail() { function fail(originalAttrOrMethod: unknown, pluginId: string) {
logWarning(`Plugin ${pluginId} tried to access iframe.${property}`, {
pluginId,
attrOrMethod: property,
entity: 'iframe',
});
if (monitorOnly) {
return originalAttrOrMethod;
}
return () => { return () => {
throw new Error('iframe.' + property + ' is not allowed in sandboxed plugins'); throw new Error('iframe.' + property + ' is not allowed in sandboxed plugins');
}; };
} }
if (descriptor.value) { if (descriptor.value) {
distortions.set(descriptor.value, fail); distortions.set(descriptor.value, fail);
} }
@ -107,20 +131,23 @@ function distortIframeAttributes(distortions: DistortionMap) {
function distortConsole(distortions: DistortionMap) { function distortConsole(distortions: DistortionMap) {
const descriptor = Object.getOwnPropertyDescriptor(window, 'console'); const descriptor = Object.getOwnPropertyDescriptor(window, 'console');
if (descriptor?.value) { if (descriptor?.value) {
function sandboxLog(...args: unknown[]) { function getSandboxConsole(originalAttrOrMethod: unknown, pluginId: string) {
console.log(`[plugin]`, ...args); // we don't monitor the console because we expect a high volume of calls
} if (monitorOnly) {
const sandboxConsole = { return originalAttrOrMethod;
log: sandboxLog, }
warn: sandboxLog,
error: sandboxLog,
info: sandboxLog,
debug: sandboxLog,
table: sandboxLog,
};
function getSandboxConsole() { function sandboxLog(...args: unknown[]) {
return sandboxConsole; console.log(`[plugin ${pluginId}]`, ...args);
}
return {
log: sandboxLog,
warn: sandboxLog,
error: sandboxLog,
info: sandboxLog,
debug: sandboxLog,
table: sandboxLog,
};
} }
distortions.set(descriptor.value, getSandboxConsole); distortions.set(descriptor.value, getSandboxConsole);
@ -132,9 +159,19 @@ function distortConsole(distortions: DistortionMap) {
// set distortions to alert to always output to the console // set distortions to alert to always output to the console
function distortAlert(distortions: DistortionMap) { function distortAlert(distortions: DistortionMap) {
function getAlertDistortion() { function getAlertDistortion(originalAttrOrMethod: unknown, pluginId: string) {
logWarning(`Plugin ${pluginId} accessed window.alert`, {
pluginId,
attrOrMethod: 'alert',
entity: 'window',
});
if (monitorOnly) {
return originalAttrOrMethod;
}
return function (...args: unknown[]) { return function (...args: unknown[]) {
console.log(`[plugin]`, ...args); console.log(`[plugin ${pluginId}]`, ...args);
}; };
} }
const descriptor = Object.getOwnPropertyDescriptor(window, 'alert'); const descriptor = Object.getOwnPropertyDescriptor(window, 'alert');
@ -147,12 +184,22 @@ function distortAlert(distortions: DistortionMap) {
} }
function distortInnerHTML(distortions: DistortionMap) { function distortInnerHTML(distortions: DistortionMap) {
function getInnerHTMLDistortion(originalMethod: unknown) { function getInnerHTMLDistortion(originalMethod: unknown, pluginId: string) {
return function innerHTMLDistortion(this: HTMLElement, ...args: string[]) { return function innerHTMLDistortion(this: HTMLElement, ...args: string[]) {
for (const arg of args) { for (const arg of args) {
const lowerCase = arg?.toLowerCase() || ''; const lowerCase = arg?.toLowerCase() || '';
for (const forbiddenElement of forbiddenElements) { for (const forbiddenElement of forbiddenElements) {
if (lowerCase.includes('<' + forbiddenElement)) { if (lowerCase.includes('<' + forbiddenElement)) {
logWarning(`Plugin ${pluginId} tried to set ${forbiddenElement} in innerHTML`, {
pluginId,
attrOrMethod: 'innerHTML',
param: forbiddenElement,
entity: 'HTMLElement',
});
if (monitorOnly) {
continue;
}
throw new Error('<' + forbiddenElement + '> is not allowed in sandboxed plugins'); throw new Error('<' + forbiddenElement + '> is not allowed in sandboxed plugins');
} }
} }
@ -181,10 +228,18 @@ function distortInnerHTML(distortions: DistortionMap) {
} }
function distortCreateElement(distortions: DistortionMap) { function distortCreateElement(distortions: DistortionMap) {
function getCreateElementDistortion(originalMethod: unknown) { function getCreateElementDistortion(originalMethod: unknown, pluginId: string) {
return function createElementDistortion(this: HTMLElement, arg?: string, options?: unknown) { return function createElementDistortion(this: HTMLElement, arg?: string, options?: unknown) {
if (arg && forbiddenElements.includes(arg)) { if (arg && forbiddenElements.includes(arg)) {
return document.createDocumentFragment(); logWarning(`Plugin ${pluginId} tried to create ${arg}`, {
pluginId,
attrOrMethod: 'createElement',
param: arg,
entity: 'document',
});
if (!monitorOnly) {
return document.createDocumentFragment();
}
} }
if (isFunction(originalMethod)) { if (isFunction(originalMethod)) {
return originalMethod.apply(this, [arg, options]); return originalMethod.apply(this, [arg, options]);
@ -198,10 +253,20 @@ function distortCreateElement(distortions: DistortionMap) {
} }
function distortInsert(distortions: DistortionMap) { function distortInsert(distortions: DistortionMap) {
function getInsertDistortion(originalMethod: unknown) { function getInsertDistortion(originalMethod: unknown, pluginId: string) {
return function insertChildDistortion(this: HTMLElement, node?: Node, ref?: Node) { return function insertChildDistortion(this: HTMLElement, node?: Node, ref?: Node) {
if (node && forbiddenElements.includes(node.nodeName.toLowerCase())) { const nodeType = node?.nodeName?.toLowerCase() || '';
return document.createDocumentFragment();
if (node && forbiddenElements.includes(nodeType)) {
logWarning(`Plugin ${pluginId} tried to insert ${nodeType}`, {
pluginId,
attrOrMethod: 'insertChild',
param: nodeType,
entity: 'HTMLElement',
});
if (!monitorOnly) {
return document.createDocumentFragment();
}
} }
if (isFunction(originalMethod)) { if (isFunction(originalMethod)) {
return originalMethod.call(this, node, ref); return originalMethod.call(this, node, ref);
@ -209,10 +274,20 @@ function distortInsert(distortions: DistortionMap) {
}; };
} }
function getinsertAdjacentElementDistortion(originalMethod: unknown) { function getinsertAdjacentElementDistortion(originalMethod: unknown, pluginId: string) {
return function insertAdjacentElementDistortion(this: HTMLElement, position?: string, node?: Node) { return function insertAdjacentElementDistortion(this: HTMLElement, position?: string, node?: Node) {
if (node && forbiddenElements.includes(node.nodeName.toLowerCase())) { const nodeType = node?.nodeName?.toLowerCase() || '';
return document.createDocumentFragment(); if (node && forbiddenElements.includes(nodeType)) {
logWarning(`Plugin ${pluginId} tried to insert ${nodeType}`, {
pluginId,
attrOrMethod: 'insertAdjacentElement',
param: nodeType,
entity: 'HTMLElement',
});
if (!monitorOnly) {
return document.createDocumentFragment();
}
} }
if (isFunction(originalMethod)) { if (isFunction(originalMethod)) {
return originalMethod.call(this, position, node); return originalMethod.call(this, position, node);
@ -240,9 +315,23 @@ function distortInsert(distortions: DistortionMap) {
// set distortions to append elements to the document // set distortions to append elements to the document
function distortAppend(distortions: DistortionMap) { function distortAppend(distortions: DistortionMap) {
// append accepts an array of nodes to append https://developer.mozilla.org/en-US/docs/Web/API/Node/append // append accepts an array of nodes to append https://developer.mozilla.org/en-US/docs/Web/API/Node/append
function getAppendDistortion(originalMethod: unknown) { function getAppendDistortion(originalMethod: unknown, pluginId: string) {
return function appendDistortion(this: HTMLElement, ...args: Node[]) { return function appendDistortion(this: HTMLElement, ...args: Node[]) {
const acceptedNodes = args?.filter((node) => !forbiddenElements.includes(node.nodeName.toLowerCase())); let acceptedNodes = args;
const filteredAcceptedNodes = args?.filter((node) => !forbiddenElements.includes(node.nodeName.toLowerCase()));
if (!monitorOnly) {
acceptedNodes = filteredAcceptedNodes;
}
if (acceptedNodes.length !== filteredAcceptedNodes.length) {
logWarning(`Plugin ${pluginId} tried to append fobiddenElements`, {
pluginId,
attrOrMethod: 'append',
param: args?.filter((node) => forbiddenElements.includes(node.nodeName.toLowerCase()))?.join(',') || '',
entity: 'HTMLElement',
});
}
if (isFunction(originalMethod)) { if (isFunction(originalMethod)) {
originalMethod.apply(this, acceptedNodes); originalMethod.apply(this, acceptedNodes);
} }
@ -252,10 +341,20 @@ function distortAppend(distortions: DistortionMap) {
} }
// appendChild accepts a single node to add https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild // appendChild accepts a single node to add https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild
function getAppendChildDistortion(originalMethod: unknown) { function getAppendChildDistortion(originalMethod: unknown, pluginId: string) {
return function appendChildDistortion(this: HTMLElement, arg?: Node) { return function appendChildDistortion(this: HTMLElement, arg?: Node) {
if (arg && forbiddenElements.includes(arg.nodeName.toLowerCase())) { const nodeType = arg?.nodeName?.toLowerCase() || '';
return document.createDocumentFragment(); if (arg && forbiddenElements.includes(nodeType)) {
logWarning(`Plugin ${pluginId} tried to append ${nodeType}`, {
pluginId,
attrOrMethod: 'appendChild',
param: nodeType,
entity: 'HTMLElement',
});
if (!monitorOnly) {
return document.createDocumentFragment();
}
} }
if (isFunction(originalMethod)) { if (isFunction(originalMethod)) {
return originalMethod.call(this, arg); return originalMethod.call(this, arg);
@ -284,6 +383,7 @@ function distortAppend(distortions: DistortionMap) {
} }
} }
// this is not a distortion for security reasons but to make plugins using web workers work correctly.
function distortWorkers(distortions: DistortionMap) { function distortWorkers(distortions: DistortionMap) {
const descriptor = Object.getOwnPropertyDescriptor(Worker.prototype, 'postMessage'); const descriptor = Object.getOwnPropertyDescriptor(Worker.prototype, 'postMessage');
function getPostMessageDistortion(originalMethod: unknown) { function getPostMessageDistortion(originalMethod: unknown) {
@ -306,6 +406,7 @@ function distortWorkers(distortions: DistortionMap) {
} }
} }
// this is not a distortion for security reasons but to make plugins using document.defaultView work correctly.
function distortDocument(distortions: DistortionMap) { function distortDocument(distortions: DistortionMap) {
const descriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'defaultView'); const descriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'defaultView');
if (descriptor?.get) { if (descriptor?.get) {

View File

@ -1,10 +1,13 @@
import { isNearMembraneProxy, ProxyTarget } from '@locker/near-membrane-shared'; import { isNearMembraneProxy, ProxyTarget } from '@locker/near-membrane-shared';
import { config } from '@grafana/runtime';
import { forbiddenElements } from './constants'; import { forbiddenElements } from './constants';
import { isReactClassComponent } from './utils'; import { isReactClassComponent, logWarning } from './utils';
// IMPORTANT: NEVER export this symbol from a public (e.g `@grafana/*`) package // IMPORTANT: NEVER export this symbol from a public (e.g `@grafana/*`) package
const SANDBOX_LIVE_VALUE = Symbol.for('@@SANDBOX_LIVE_VALUE'); const SANDBOX_LIVE_VALUE = Symbol.for('@@SANDBOX_LIVE_VALUE');
const monitorOnly = Boolean(config.featureToggles.frontendSandboxMonitorOnly);
export function getSafeSandboxDomElement(element: Element, pluginId: string): Element { export function getSafeSandboxDomElement(element: Element, pluginId: string): Element {
const nodeName = Reflect.get(element, 'nodeName'); const nodeName = Reflect.get(element, 'nodeName');
@ -26,7 +29,14 @@ export function getSafeSandboxDomElement(element: Element, pluginId: string): El
} }
if (forbiddenElements.includes(nodeName)) { if (forbiddenElements.includes(nodeName)) {
throw new Error('<' + nodeName + '> is not allowed in sandboxed plugins'); logWarning('<' + nodeName + '> is not allowed in sandboxed plugins', {
pluginId,
param: nodeName,
});
if (!monitorOnly) {
throw new Error('<' + nodeName + '> is not allowed in sandboxed plugins');
}
} }
// allow elements inside the sandbox or the sandbox body // allow elements inside the sandbox or the sandbox body
@ -38,10 +48,15 @@ export function getSafeSandboxDomElement(element: Element, pluginId: string): El
return element; return element;
} }
// any other element gets a mock if (!monitorOnly) {
const mockElement = document.createElement(nodeName); // any other element gets a mock
mockElement.dataset.grafanaPluginSandboxElement = 'true'; const mockElement = document.createElement(nodeName);
return mockElement; mockElement.dataset.grafanaPluginSandboxElement = 'true';
// we are not logging this because a high number of warnings can be generated
return mockElement;
} else {
return element;
}
} }
export function isDomElement(obj: unknown): obj is Element { export function isDomElement(obj: unknown): obj is Element {

View File

@ -18,6 +18,7 @@ import {
import { sandboxPluginDependencies } from './plugin_dependencies'; import { sandboxPluginDependencies } from './plugin_dependencies';
import { sandboxPluginComponents } from './sandbox_components'; import { sandboxPluginComponents } from './sandbox_components';
import { CompartmentDependencyModule, PluginFactoryFunction } from './types'; import { CompartmentDependencyModule, PluginFactoryFunction } from './types';
import { logError } from './utils';
// Loads near membrane custom formatter for near membrane proxy objects. // Loads near membrane custom formatter for near membrane proxy objects.
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
@ -34,7 +35,12 @@ export async function importPluginModuleInSandbox({ pluginId }: { pluginId: stri
} }
return pluginImportCache.get(pluginId); return pluginImportCache.get(pluginId);
} catch (e) { } catch (e) {
throw new Error(`Could not import plugin ${pluginId} inside sandbox: ` + e); const error = new Error(`Could not import plugin ${pluginId} inside sandbox: ` + e);
logError(error, {
pluginId,
error: String(e),
});
throw error;
} }
} }
@ -56,7 +62,7 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise<unknown>
} }
const distortion = generalDistortionMap.get(originalValue); const distortion = generalDistortionMap.get(originalValue);
if (distortion) { if (distortion) {
return distortion(originalValue) as ProxyTarget; return distortion(originalValue, meta.id) as ProxyTarget;
} }
return originalValue; return originalValue;
} }
@ -100,7 +106,12 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise<unknown>
const pluginExports = await sandboxPluginComponents(pluginExportsRaw, meta); const pluginExports = await sandboxPluginComponents(pluginExportsRaw, meta);
resolve(pluginExports); resolve(pluginExports);
} catch (e) { } catch (e) {
reject(new Error(`Could not execute plugin ${meta.id}: ` + e)); const error = new Error(`Could not execute plugin's define ${meta.id}: ` + e);
logError(error, {
pluginId: meta.id,
error: String(e),
});
reject(error);
} }
}, },
}), }),
@ -137,7 +148,12 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise<unknown>
// of endowments. // of endowments.
sandboxEnvironment.evaluate(pluginCode); sandboxEnvironment.evaluate(pluginCode);
} catch (e) { } catch (e) {
reject(new Error(`Could not execute plugin ${meta.id}: ` + e)); const error = new Error(`Could not run plugin ${meta.id} inside sandbox: ` + e);
logError(error, {
pluginId: meta.id,
error: String(e),
});
reject(error);
} }
}); });
} }

View File

@ -1,7 +1,12 @@
import React from 'react'; import React from 'react';
import { LogContext } from '@grafana/faro-web-sdk';
import { logWarning as logWarningRuntime, logError as logErrorRuntime, config } from '@grafana/runtime';
import { SandboxedPluginObject } from './types'; import { SandboxedPluginObject } from './types';
const monitorOnly = Boolean(config.featureToggles.frontendSandboxMonitorOnly);
export function isSandboxedPluginObject(value: unknown): value is SandboxedPluginObject { export function isSandboxedPluginObject(value: unknown): value is SandboxedPluginObject {
return !!value && typeof value === 'object' && value?.hasOwnProperty('plugin'); return !!value && typeof value === 'object' && value?.hasOwnProperty('plugin');
} }
@ -13,3 +18,21 @@ export function assertNever(x: never): never {
export function isReactClassComponent(obj: unknown): obj is React.Component { export function isReactClassComponent(obj: unknown): obj is React.Component {
return obj instanceof React.Component; return obj instanceof React.Component;
} }
export function logWarning(message: string, context?: LogContext) {
context = {
...context,
source: 'sandbox',
monitorOnly: String(monitorOnly),
};
logWarningRuntime(message, context);
}
export function logError(error: Error, context?: LogContext) {
context = {
...context,
source: 'sandbox',
monitorOnly: String(monitorOnly),
};
logErrorRuntime(error, context);
}