Frontend o11y: Report browser crashes to Faro (#95772)

* Report browser crashes to Faro

* Fix linting

* Change context log context prefix

* Update types

* Update crash detection library to report stale tabs

* Post merge fixes
This commit is contained in:
Piotr Jamróz 2024-11-12 16:07:27 +01:00 committed by GitHub
parent 6e7de36a67
commit 3a6858cf26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 196 additions and 1 deletions

View File

@ -222,6 +222,7 @@ Experimental features might be changed or removed without prior notice.
| `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode |
| `zipkinBackendMigration` | Enables querying Zipkin data source without the proxy |
| `enableSCIM` | Enables SCIM support for user and group management |
| `crashDetection` | Enables browser crash detection reporting to Faro. |
## Development feature toggles

View File

@ -165,6 +165,7 @@
"codeowners": "^5.1.1",
"copy-webpack-plugin": "12.0.2",
"core-js": "3.38.1",
"crashme": "0.0.15",
"css-loader": "7.1.2",
"css-minimizer-webpack-plugin": "6.0.0",
"cypress": "13.10.0",

View File

@ -236,4 +236,5 @@ export interface FeatureToggles {
enableExtensionsAdminPage?: boolean;
zipkinBackendMigration?: boolean;
enableSCIM?: boolean;
crashDetection?: boolean;
}

View File

@ -1628,6 +1628,13 @@ var (
Stage: FeatureStageExperimental,
Owner: identityAccessTeam,
},
{
Name: "crashDetection",
Description: "Enables browser crash detection reporting to Faro.",
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityTracesAndProfilingSquad,
FrontendOnly: true,
},
}
)

View File

@ -217,3 +217,4 @@ exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,fals
enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false
zipkinBackendMigration,experimental,@grafana/oss-big-tent,false,false,false
enableSCIM,experimental,@grafana/identity-access-team,false,false,false
crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
217 enableExtensionsAdminPage experimental @grafana/plugins-platform-backend false true false
218 zipkinBackendMigration experimental @grafana/oss-big-tent false false false
219 enableSCIM experimental @grafana/identity-access-team false false false
220 crashDetection experimental @grafana/observability-traces-and-profiling false false true

View File

@ -878,4 +878,8 @@ const (
// FlagEnableSCIM
// Enables SCIM support for user and group management
FlagEnableSCIM = "enableSCIM"
// FlagCrashDetection
// Enables browser crash detection reporting to Faro.
FlagCrashDetection = "crashDetection"
)

View File

@ -855,6 +855,19 @@
"expression": "true"
}
},
{
"metadata": {
"name": "crashDetection",
"resourceVersion": "1730381712885",
"creationTimestamp": "2024-10-31T13:35:12Z"
},
"spec": {
"description": "Enables browser crash detection reporting to Faro.",
"stage": "experimental",
"codeowner": "@grafana/observability-traces-and-profiling",
"frontend": true
}
},
{
"metadata": {
"name": "dashboardNewLayouts",
@ -3470,4 +3483,4 @@
}
}
]
}
}

View File

@ -56,6 +56,7 @@ import { AppChromeService } from './core/components/AppChrome/AppChromeService';
import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
import { PluginPage } from './core/components/Page/PluginPage';
import { GrafanaContextType, useChromeHeaderHeight, useReturnToPreviousInternal } from './core/context/GrafanaContext';
import { initializeCrashDetection } from './core/crash';
import { initIconCache } from './core/icons/iconBundle';
import { initializeI18n } from './core/internationalization';
import { setMonacoEnv } from './core/monacoEnv';
@ -267,6 +268,10 @@ export class GrafanaApp {
initializeScopes();
if (config.featureToggles.crashDetection) {
initializeCrashDetection();
}
const root = createRoot(document.getElementById('reactRoot')!);
root.render(
createElement(AppWrapper, {

View File

@ -0,0 +1,7 @@
import { initClientWorker } from 'crashme';
initClientWorker({
dbName: 'grafana.crashes',
// How often the tab will report its state
pingInterval: 1000,
});

View File

@ -0,0 +1,58 @@
import { LogContext } from '@grafana/faro-core/dist/types/api/logs/types';
export interface ChromePerformanceMemory {
totalJSHeapSize: number;
usedJSHeapSize: number;
jsHeapSizeLimit: number;
}
export interface ChromePerformance {
memory: ChromePerformanceMemory;
}
function isChromePerformanceMemory(memory: unknown): memory is ChromePerformanceMemory {
if (!memory || typeof memory !== 'object') {
return false;
}
return 'totalJSHeapSize' in memory && 'usedJSHeapSize' in memory && 'jsHeapSizeLimit' in memory;
}
export function isChromePerformance(performance: unknown): performance is ChromePerformance {
if (!performance || typeof performance !== 'object') {
return false;
}
return 'memory' in performance && isChromePerformanceMemory(performance.memory);
}
/**
* Ensures the context is a flat object with strings (required by Faro)
*/
export function prepareContext(context: Object): LogContext {
const preparedContext: LogContext = {};
function prepare(value: object | string | number, propertyName: string) {
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
throw new Error('Array values are not supported.');
} else {
for (const key in value) {
if (value.hasOwnProperty(key)) {
// @ts-ignore
prepare(value[key], propertyName ? `${propertyName}_${key}` : key);
}
}
}
} else if (typeof value === 'string') {
preparedContext[propertyName] = value;
} else if (typeof value === 'number') {
if (Number.isInteger(value)) {
preparedContext[propertyName] = value.toString();
} else {
preparedContext[propertyName] = value.toFixed(4);
}
}
}
prepare(context, 'crash');
return preparedContext;
}

View File

@ -0,0 +1,8 @@
import { initDetectorWorker } from 'crashme';
initDetectorWorker({
dbName: 'grafana.crashes',
interval: 5000,
crashThreshold: 5000,
staleThreshold: 5000,
});

View File

@ -0,0 +1,81 @@
import { initCrashDetection } from 'crashme';
import { BaseStateReport } from 'crashme/dist/types';
import { nanoid } from 'nanoid';
import { config, createMonitoringLogger } from '@grafana/runtime';
import { contextSrv } from '../services/context_srv';
import { isChromePerformance, prepareContext } from './crash.utils';
const logger = createMonitoringLogger('core.crash-detection');
interface GrafanaCrashReport extends BaseStateReport {
app: {
version: string;
url: string;
};
user: {
email: string;
login: string;
name: string;
};
memory?: {
heapUtilization: number;
limitUtilization: number;
usedJSHeapSize: number;
totalJSHeapSize: number;
jsHeapSizeLimit: number;
};
}
export function initializeCrashDetection() {
initCrashDetection<GrafanaCrashReport>({
id: nanoid(5),
dbName: 'grafana.crashes',
createClientWorker(): Worker {
return new Worker(new URL('./client.worker', import.meta.url));
},
createDetectorWorker(): SharedWorker {
return new SharedWorker(new URL('./detector.worker', import.meta.url));
},
reportCrash: async (report) => {
const preparedContext = prepareContext(report);
logger.logWarning('browser crash detected', preparedContext);
return true;
},
reportStaleTab: async (report) => {
const preparedContext = prepareContext(report);
logger.logWarning('stale browser tab detected', preparedContext);
return true;
},
updateInfo: (info) => {
info.app = {
version: config.buildInfo.version,
url: window.location.href,
};
info.user = {
email: contextSrv.user.email,
login: contextSrv.user.login,
name: contextSrv.user.name,
};
if (isChromePerformance(performance)) {
info.memory = {
heapUtilization: performance.memory.usedJSHeapSize / performance.memory.totalJSHeapSize,
limitUtilization: performance.memory.totalJSHeapSize / performance.memory.jsHeapSizeLimit,
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
};
}
},
});
}

View File

@ -14324,6 +14324,13 @@ __metadata:
languageName: node
linkType: hard
"crashme@npm:0.0.15":
version: 0.0.15
resolution: "crashme@npm:0.0.15"
checksum: 10/576110b3d61f996869b993c2f6b8dca33ac82d07ea708b39217b6349653e38a9894c1af838136bc60ba61f6c69bd68f1c60e3da772cb2fa1fe0b64c2fbc53b79
languageName: node
linkType: hard
"create-jest@npm:^29.7.0":
version: 29.7.0
resolution: "create-jest@npm:29.7.0"
@ -18878,6 +18885,7 @@ __metadata:
common-tags: "npm:1.8.2"
copy-webpack-plugin: "npm:12.0.2"
core-js: "npm:3.38.1"
crashme: "npm:0.0.15"
css-loader: "npm:7.1.2"
css-minimizer-webpack-plugin: "npm:6.0.0"
cypress: "npm:13.10.0"