mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Panel monitoring (#75456)
* WIP * remove bug * XY Chart logs * wip * wip * wip * wip * wip * Revert experimental logs * wip dataviz options monitor * add logging functionality on panel save * remove unused file * readd start load time * remove afterFrame lib. remove assertions where possible * add tests * PR modifications * fix betterer * rename logEvent to logPanelEvent * add feature flag * split monitor into measurement and logging parts * rename component * log panel options on error capture also. Log overrides only then * refactor logs * log panel option changes only on error in panel edit mode * refactor function
This commit is contained in:
parent
150d4d68ad
commit
ef82767dab
@ -143,6 +143,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins |
|
||||
| `httpSLOLevels` | Adds SLO level to http request metrics |
|
||||
| `alertingModifiedExport` | Enables using UI for provisioned rules modification and export |
|
||||
| `panelMonitoring` | Enables panel monitoring through logs and measurements |
|
||||
| `enableNativeHTTPHistogram` | Enables native HTTP Histograms |
|
||||
| `transformationsVariableSupport` | Allows using variables in transformations |
|
||||
| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists |
|
||||
|
@ -136,6 +136,7 @@ export interface FeatureToggles {
|
||||
cloudWatchWildCardDimensionValues?: boolean;
|
||||
externalServiceAccounts?: boolean;
|
||||
alertingModifiedExport?: boolean;
|
||||
panelMonitoring?: boolean;
|
||||
enableNativeHTTPHistogram?: boolean;
|
||||
transformationsVariableSupport?: boolean;
|
||||
kubernetesPlaylists?: boolean;
|
||||
|
@ -824,6 +824,13 @@ var (
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaAlertingSquad,
|
||||
},
|
||||
{
|
||||
Name: "panelMonitoring",
|
||||
Description: "Enables panel monitoring through logs and measurements",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaDatavizSquad,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "enableNativeHTTPHistogram",
|
||||
Description: "Enables native HTTP Histograms",
|
||||
|
@ -117,6 +117,7 @@ idForwarding,experimental,@grafana/grafana-authnz-team,true,false,false,false
|
||||
cloudWatchWildCardDimensionValues,GA,@grafana/aws-datasources,false,false,false,false
|
||||
externalServiceAccounts,experimental,@grafana/grafana-authnz-team,true,false,false,false
|
||||
alertingModifiedExport,experimental,@grafana/alerting-squad,false,false,false,false
|
||||
panelMonitoring,experimental,@grafana/dataviz-squad,false,false,false,true
|
||||
enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false,false,false
|
||||
transformationsVariableSupport,experimental,@grafana/grafana-bi-squad,false,false,false,true
|
||||
kubernetesPlaylists,experimental,@grafana/grafana-app-platform-squad,false,false,false,true
|
||||
|
|
@ -479,6 +479,10 @@ const (
|
||||
// Enables using UI for provisioned rules modification and export
|
||||
FlagAlertingModifiedExport = "alertingModifiedExport"
|
||||
|
||||
// FlagPanelMonitoring
|
||||
// Enables panel monitoring through logs and measurements
|
||||
FlagPanelMonitoring = "panelMonitoring"
|
||||
|
||||
// FlagEnableNativeHTTPHistogram
|
||||
// Enables native HTTP Histograms
|
||||
FlagEnableNativeHTTPHistogram = "enableNativeHTTPHistogram"
|
||||
|
18
public/app/core/log_events.ts
Normal file
18
public/app/core/log_events.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export enum PanelLogEvents {
|
||||
FIELD_CONFIG_OVERRIDES_CHANGED_EVENT = 'field config overrides changed',
|
||||
NEW_PANEL_OPTION_EVENT = 'new panel option',
|
||||
PANEL_OPTION_CHANGED_EVENT = 'panel option changed',
|
||||
NEW_DEFAULT_FIELD_CONFIG_EVENT = 'new default field config',
|
||||
DEFAULT_FIELD_CONFIG_CHANGED_EVENT = 'default field config changed',
|
||||
NEW_CUSTOM_FIELD_CONFIG_EVENT = 'new custom field config',
|
||||
CUSTOM_FIELD_CONFIG_CHANGED_EVENT = 'custom field config changed',
|
||||
MEASURE_PANEL_LOAD_TIME_EVENT = 'measure panel load time',
|
||||
THRESHOLDS_COUNT_CHANGED_EVENT = 'thresholds count changed',
|
||||
THRESHOLDS_MODE_CHANGED_EVENT = 'thresholds mode changed',
|
||||
MAPPINGS_COUNT_CHANGED_EVENT = 'mappings count changed',
|
||||
LINKS_COUNT_CHANGED_EVENT = 'links count changed',
|
||||
PANEL_ERROR = 'panel error',
|
||||
}
|
||||
|
||||
export const FIELD_CONFIG_OVERRIDES_KEY = 'overrides';
|
||||
export const FIELD_CONFIG_CUSTOM_KEY = 'custom';
|
@ -0,0 +1,49 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
const mockPushMeasurement = jest.fn();
|
||||
|
||||
import { PanelLoadTimeMonitor } from './PanelLoadTimeMonitor';
|
||||
|
||||
jest.mock('app/core/config', () => ({
|
||||
config: {
|
||||
grafanaJavascriptAgent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/faro-web-sdk', () => ({
|
||||
faro: {
|
||||
api: {
|
||||
pushMeasurement: mockPushMeasurement,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('PanelLoadTimeMonitor', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('logs load time measurement on render', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const props = {
|
||||
isInPanelEdit: true,
|
||||
panelType: 'timeseries',
|
||||
panelId: 1,
|
||||
panelTitle: 'Panel Title',
|
||||
panelOptions: {},
|
||||
panelFieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
};
|
||||
|
||||
render(<PanelLoadTimeMonitor {...props} />);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(mockPushMeasurement).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,50 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { faro } from '@grafana/faro-web-sdk';
|
||||
import { config } from 'app/core/config';
|
||||
import { PanelLogEvents } from 'app/core/log_events';
|
||||
|
||||
interface Props {
|
||||
panelType: string;
|
||||
panelId: number;
|
||||
panelTitle: string;
|
||||
}
|
||||
|
||||
export const PanelLoadTimeMonitor = (props: Props) => {
|
||||
const startLoadTime = performance.now();
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.grafanaJavascriptAgent.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This code will be run ASAP after Style and Layout information have
|
||||
// been calculated and the paint has occurred.
|
||||
// https://firefox-source-docs.mozilla.org/performance/bestpractices.html
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
faro.api.pushMeasurement(
|
||||
{
|
||||
type: PanelLogEvents.MEASURE_PANEL_LOAD_TIME_EVENT,
|
||||
values: {
|
||||
start_loading_time_ms: startLoadTime,
|
||||
load_time_ms: performance.now() - startLoadTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
context: {
|
||||
panel_type: props.panelType,
|
||||
panel_id: String(props.panelId),
|
||||
panel_title: props.panelTitle,
|
||||
},
|
||||
}
|
||||
);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
@ -30,6 +30,7 @@ import {
|
||||
SeriesVisibilityChangeMode,
|
||||
AdHocFilterItem,
|
||||
} from '@grafana/ui';
|
||||
import config from 'app/core/config';
|
||||
import { profiler } from 'app/core/profiler';
|
||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
@ -47,8 +48,10 @@ import { getPanelChromeProps } from '../utils/getPanelChromeProps';
|
||||
import { loadSnapshotData } from '../utils/loadSnapshotData';
|
||||
|
||||
import { PanelHeaderMenuWrapper } from './PanelHeader/PanelHeaderMenuWrapper';
|
||||
import { PanelLoadTimeMonitor } from './PanelLoadTimeMonitor';
|
||||
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
|
||||
import { liveTimer } from './liveTimer';
|
||||
import { PanelOptionsLogger } from './panelOptionsLogger';
|
||||
|
||||
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||
|
||||
@ -81,6 +84,7 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
private readonly timeSrv: TimeSrv = getTimeSrv();
|
||||
private subs = new Subscription();
|
||||
private eventFilter: EventFilterOptions = { onlyLocal: true };
|
||||
private panelOptionsLogger: PanelOptionsLogger | undefined = undefined;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@ -112,6 +116,16 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
},
|
||||
data: this.getInitialPanelDataState(),
|
||||
};
|
||||
|
||||
if (config.featureToggles.panelMonitoring && this.getPanelContextApp() === CoreApp.PanelEditor) {
|
||||
const panelInfo = {
|
||||
panelId: String(props.panel.id),
|
||||
panelType: props.panel.type,
|
||||
panelTitle: props.panel.title,
|
||||
};
|
||||
|
||||
this.panelOptionsLogger = new PanelOptionsLogger(props.panel.getOptions(), props.panel.fieldConfig, panelInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Due to a mutable panel model we get the sync settings via function that proactively reads from the model
|
||||
@ -373,8 +387,17 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
this.props.panel.updateFieldConfig(config);
|
||||
};
|
||||
|
||||
logPanelChangesOnError() {
|
||||
this.panelOptionsLogger!.logChanges(this.props.panel.getOptions(), this.props.panel.fieldConfig);
|
||||
}
|
||||
|
||||
onPanelError = (error: Error) => {
|
||||
if (config.featureToggles.panelMonitoring && this.getPanelContextApp() === CoreApp.PanelEditor) {
|
||||
this.logPanelChangesOnError();
|
||||
}
|
||||
|
||||
const errorMessage = error.message || DEFAULT_PLUGIN_ERROR;
|
||||
|
||||
if (this.state.errorMessage !== errorMessage) {
|
||||
this.setState({ errorMessage });
|
||||
}
|
||||
@ -512,6 +535,9 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
onChangeTimeRange={this.onChangeTimeRange}
|
||||
eventBus={dashboard.events}
|
||||
/>
|
||||
{config.featureToggles.panelMonitoring && this.state.errorMessage === undefined && (
|
||||
<PanelLoadTimeMonitor panelType={plugin.meta.id} panelId={panel.id} panelTitle={panel.title} />
|
||||
)}
|
||||
</PanelContextProvider>
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,180 @@
|
||||
import { GraphDrawStyle, GraphGradientMode } from '@grafana/schema';
|
||||
|
||||
const mockPushEvent = jest.fn();
|
||||
|
||||
import { PanelOptionsLogger } from './panelOptionsLogger';
|
||||
|
||||
jest.mock('@grafana/faro-web-sdk', () => ({
|
||||
faro: {
|
||||
api: {
|
||||
pushEvent: mockPushEvent,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('app/core/config', () => ({
|
||||
config: {
|
||||
grafanaJavascriptAgent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('OptionsPane', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('logs panel options', () => {
|
||||
const oldPanelOptions = {
|
||||
showHeader: true,
|
||||
footer: {
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
|
||||
const newPanelOptions = {
|
||||
showHeader: false,
|
||||
footer: {
|
||||
show: false,
|
||||
},
|
||||
showTypeIcons: true,
|
||||
};
|
||||
|
||||
const panelInfo = {
|
||||
panelType: 'table',
|
||||
panelId: '1',
|
||||
panelTitle: 'Panel Title',
|
||||
};
|
||||
|
||||
const expectedLogResults = [
|
||||
{
|
||||
key: 'showHeader',
|
||||
newValue: 'false',
|
||||
oldValue: 'true',
|
||||
panelTitle: 'Panel Title',
|
||||
panelId: '1',
|
||||
panelType: 'table',
|
||||
},
|
||||
{
|
||||
key: 'footer',
|
||||
newValue: '{"show":false}',
|
||||
oldValue: '{"show":true}',
|
||||
panelTitle: 'Panel Title',
|
||||
panelId: '1',
|
||||
panelType: 'table',
|
||||
},
|
||||
{
|
||||
key: 'showTypeIcons',
|
||||
newValue: 'true',
|
||||
oldValue: '',
|
||||
panelTitle: 'Panel Title',
|
||||
panelId: '1',
|
||||
panelType: 'table',
|
||||
},
|
||||
];
|
||||
|
||||
const panelOptionsLogger = new PanelOptionsLogger(oldPanelOptions, { defaults: {}, overrides: [] }, panelInfo);
|
||||
|
||||
panelOptionsLogger.logChanges(newPanelOptions, { defaults: {}, overrides: [] });
|
||||
|
||||
expect(mockPushEvent).toHaveBeenCalledTimes(3);
|
||||
expect(mockPushEvent.mock.calls).toEqual([
|
||||
['panel option changed', expectedLogResults[0]],
|
||||
['panel option changed', expectedLogResults[1]],
|
||||
['new panel option', expectedLogResults[2]],
|
||||
]);
|
||||
});
|
||||
it('logs field config changes', () => {
|
||||
const oldFieldConfig = {
|
||||
defaults: {
|
||||
unit: 'bytes',
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Bars,
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
matcher: {
|
||||
id: 'byName',
|
||||
options: '',
|
||||
},
|
||||
properties: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const newFieldConfig = {
|
||||
defaults: {
|
||||
unit: 'metres',
|
||||
newField: 'newValue',
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Line,
|
||||
gradientMode: GraphGradientMode.Hue,
|
||||
},
|
||||
},
|
||||
overrides: [],
|
||||
};
|
||||
|
||||
const panelInfo = {
|
||||
panelType: 'timeseries',
|
||||
panelId: '1',
|
||||
panelTitle: 'Panel Title',
|
||||
};
|
||||
|
||||
const expectedLogResults = [
|
||||
{
|
||||
key: 'overrides',
|
||||
newValue: '[]',
|
||||
oldValue: '[{"matcher":{"id":"byName","options":""},"properties":[]}]',
|
||||
panelTitle: 'Panel Title',
|
||||
panelId: '1',
|
||||
panelType: 'timeseries',
|
||||
},
|
||||
{
|
||||
key: 'unit',
|
||||
newValue: 'metres',
|
||||
oldValue: 'bytes',
|
||||
panelTitle: 'Panel Title',
|
||||
panelId: '1',
|
||||
panelType: 'timeseries',
|
||||
},
|
||||
{
|
||||
key: 'newField',
|
||||
newValue: 'newValue',
|
||||
oldValue: '',
|
||||
panelTitle: 'Panel Title',
|
||||
panelId: '1',
|
||||
panelType: 'timeseries',
|
||||
},
|
||||
{
|
||||
key: 'drawStyle',
|
||||
newValue: 'line',
|
||||
oldValue: 'bars',
|
||||
panelTitle: 'Panel Title',
|
||||
panelId: '1',
|
||||
panelType: 'timeseries',
|
||||
},
|
||||
{
|
||||
key: 'gradientMode',
|
||||
newValue: 'hue',
|
||||
oldValue: '',
|
||||
panelTitle: 'Panel Title',
|
||||
panelId: '1',
|
||||
panelType: 'timeseries',
|
||||
},
|
||||
];
|
||||
|
||||
const panelOptionsLogger = new PanelOptionsLogger({}, oldFieldConfig, panelInfo);
|
||||
|
||||
panelOptionsLogger.logChanges({}, newFieldConfig);
|
||||
|
||||
expect(mockPushEvent).toHaveBeenCalledTimes(5);
|
||||
expect(mockPushEvent.mock.calls).toEqual([
|
||||
['field config overrides changed', expectedLogResults[0]],
|
||||
['default field config changed', expectedLogResults[1]],
|
||||
['new default field config', expectedLogResults[2]],
|
||||
['custom field config changed', expectedLogResults[3]],
|
||||
['new custom field config', expectedLogResults[4]],
|
||||
]);
|
||||
});
|
||||
});
|
122
public/app/features/dashboard/dashgrid/panelOptionsLogger.ts
Normal file
122
public/app/features/dashboard/dashgrid/panelOptionsLogger.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { FieldConfigSource } from '@grafana/data';
|
||||
import { faro } from '@grafana/faro-web-sdk';
|
||||
import { FIELD_CONFIG_CUSTOM_KEY, FIELD_CONFIG_OVERRIDES_KEY, PanelLogEvents } from 'app/core/log_events';
|
||||
|
||||
interface PanelLogInfo {
|
||||
panelId: string;
|
||||
panelType: string;
|
||||
panelTitle: string;
|
||||
}
|
||||
|
||||
export class PanelOptionsLogger {
|
||||
private initialPanelOptions: unknown;
|
||||
private initialFieldConfig: FieldConfigSource;
|
||||
private panelLogInfo: PanelLogInfo;
|
||||
|
||||
constructor(initialPanelOptions: unknown, initialFieldConfig: FieldConfigSource, panelLogInfo: PanelLogInfo) {
|
||||
this.initialPanelOptions = initialPanelOptions;
|
||||
this.initialFieldConfig = initialFieldConfig;
|
||||
this.panelLogInfo = panelLogInfo;
|
||||
}
|
||||
|
||||
logChanges = (latestPanelOptions: unknown, latestFieldConfig: FieldConfigSource) => {
|
||||
this.logPanelOptionChanges(latestPanelOptions, this.initialPanelOptions);
|
||||
this.logFieldConfigChanges(latestFieldConfig, this.initialFieldConfig);
|
||||
|
||||
//set the old values to the current values for next log diff
|
||||
this.initialPanelOptions = latestPanelOptions;
|
||||
this.initialFieldConfig = latestFieldConfig;
|
||||
};
|
||||
|
||||
logPanelEvent = (eventName: string, newKey: string, newVal: string, oldVal?: string) => {
|
||||
const logObj = {
|
||||
key: newKey,
|
||||
newValue: newVal,
|
||||
oldValue: oldVal ?? '',
|
||||
panelTitle: this.panelLogInfo.panelTitle,
|
||||
panelId: this.panelLogInfo.panelId,
|
||||
panelType: this.panelLogInfo.panelType,
|
||||
};
|
||||
|
||||
faro.api.pushEvent(eventName, logObj);
|
||||
};
|
||||
|
||||
logPanelOptionChanges = (panelOptions: unknown, oldPanelOptions: unknown) => {
|
||||
if (typeof panelOptions !== 'object' || panelOptions === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof oldPanelOptions !== 'object' || oldPanelOptions === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPanelOptionsUnknown: { [key: string]: unknown } = { ...oldPanelOptions };
|
||||
|
||||
for (const [key, value] of Object.entries(panelOptions)) {
|
||||
const newValue: string = typeof value !== 'string' ? JSON.stringify(value) : value;
|
||||
const oldValue: string =
|
||||
typeof value !== 'string' ? JSON.stringify(oldPanelOptionsUnknown[key]) : String(oldPanelOptionsUnknown[key]);
|
||||
|
||||
if (oldPanelOptionsUnknown[key] === undefined) {
|
||||
this.logPanelEvent(PanelLogEvents.NEW_PANEL_OPTION_EVENT, key, newValue);
|
||||
} else if (oldValue !== newValue) {
|
||||
this.logPanelEvent(PanelLogEvents.PANEL_OPTION_CHANGED_EVENT, key, newValue, oldValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
logFieldConfigChanges = (fieldConfig: FieldConfigSource<unknown>, oldFieldConfig: FieldConfigSource<unknown>) => {
|
||||
// overrides are an array of objects, so stringify it all and log changes
|
||||
// in lack of an index, we can't tell which override changed
|
||||
const oldOverridesStr = JSON.stringify(oldFieldConfig.overrides);
|
||||
const newOverridesStr = JSON.stringify(fieldConfig.overrides);
|
||||
if (oldOverridesStr !== newOverridesStr) {
|
||||
this.logPanelEvent(
|
||||
PanelLogEvents.FIELD_CONFIG_OVERRIDES_CHANGED_EVENT,
|
||||
FIELD_CONFIG_OVERRIDES_KEY,
|
||||
newOverridesStr,
|
||||
oldOverridesStr
|
||||
);
|
||||
}
|
||||
|
||||
const oldDefaults: { [key: string]: unknown } = { ...oldFieldConfig.defaults };
|
||||
|
||||
// go through field config keys except custom, we treat that below
|
||||
for (const [key, value] of Object.entries(fieldConfig.defaults)) {
|
||||
if (key === FIELD_CONFIG_CUSTOM_KEY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newValue: string = typeof value !== 'string' ? JSON.stringify(value) : value;
|
||||
const oldValue: string = typeof value !== 'string' ? JSON.stringify(oldDefaults[key]) : String(oldDefaults[key]);
|
||||
|
||||
if (oldDefaults[key] === undefined) {
|
||||
this.logPanelEvent(PanelLogEvents.NEW_DEFAULT_FIELD_CONFIG_EVENT, key, newValue);
|
||||
} else if (oldValue !== newValue) {
|
||||
this.logPanelEvent(PanelLogEvents.DEFAULT_FIELD_CONFIG_CHANGED_EVENT, key, newValue, oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fieldConfig.defaults.custom || oldDefaults.custom === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldCustom: { [key: string]: unknown } = { ...oldDefaults.custom };
|
||||
|
||||
// go through custom field config keys
|
||||
for (const [key, value] of Object.entries(fieldConfig.defaults.custom)) {
|
||||
if (oldDefaults.custom === null || oldCustom[key] === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newValue: string = typeof value !== 'string' ? JSON.stringify(value) : value;
|
||||
const oldValue: string = typeof value !== 'string' ? JSON.stringify(oldCustom[key]) : String(oldCustom[key]);
|
||||
|
||||
if (oldCustom[key] === undefined) {
|
||||
this.logPanelEvent(PanelLogEvents.NEW_CUSTOM_FIELD_CONFIG_EVENT, key, newValue);
|
||||
} else if (oldValue !== newValue) {
|
||||
this.logPanelEvent(PanelLogEvents.CUSTOM_FIELD_CONFIG_CHANGED_EVENT, key, newValue, oldValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user