From 60f9ff0a071c646c52fc71b37de526038f9c29b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Mon, 2 Dec 2024 15:12:36 +0100 Subject: [PATCH] Frontend o11y: Load SharedWorkers for crash detection (#96673) * Load SharedWorkers from blob * Fix typo * Update docs * Add more docs * Simplify extending CorsSharedWorker * Revert "Simplify extending CorsSharedWorker" This reverts commit 1603e5f02f995f325011713e1bc64fb3267f150b. * Simplify extending CorsSharedWorker * Remove ts-ignore * Update betterer and add docs * Update public/app/core/crash/index.ts Co-authored-by: Sven Grossmann * Update public/app/core/utils/CorsSharedWorker.ts Co-authored-by: Sven Grossmann * Update public/app/core/crash/index.ts Co-authored-by: Sven Grossmann * Update public/app/core/utils/CorsSharedWorker.ts Co-authored-by: Sven Grossmann * Simplify getting scriptsBasePathUrl * Disable linting for SharedWorker type assertion --------- Co-authored-by: Sven Grossmann --- public/app/core/crash/index.ts | 20 ++++++++++++-- public/app/core/utils/CorsSharedWorker.ts | 33 +++++++++++++++++++++++ public/app/core/utils/CorsWorker.ts | 4 +-- 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 public/app/core/utils/CorsSharedWorker.ts diff --git a/public/app/core/crash/index.ts b/public/app/core/crash/index.ts index a2f54a9d866..e3ef01ed25d 100644 --- a/public/app/core/crash/index.ts +++ b/public/app/core/crash/index.ts @@ -3,8 +3,10 @@ import { BaseStateReport } from 'crashme/dist/types'; import { nanoid } from 'nanoid'; import { config, createMonitoringLogger } from '@grafana/runtime'; +import { CorsWorker as Worker } from 'app/core/utils/CorsWorker'; import { contextSrv } from '../services/context_srv'; +import { CorsSharedWorker as SharedWorker, sharedWorkersSupported } from '../utils/CorsSharedWorker'; import { isChromePerformance, prepareContext } from './crash.utils'; @@ -30,6 +32,10 @@ interface GrafanaCrashReport extends BaseStateReport { } export function initializeCrashDetection() { + if (!sharedWorkersSupported()) { + return; + } + initCrashDetection({ id: nanoid(5), @@ -39,8 +45,18 @@ export function initializeCrashDetection() { return new Worker(new URL('./client.worker', import.meta.url)); }, - createDetectorWorker(): SharedWorker { - return new SharedWorker(new URL('./detector.worker', import.meta.url)); + /** + * There are limitations that require us to manually assert the type here. + * 1) Webpack uses static code analysis to create a new entry point for a SharedWorker. + * It requies constructing an object with exact syntax new SharedWorker(...) (https://webpack.js.org/guides/web-workers/) + * 2) Some browsers may not support SharedWorkers hence we cannot extend CorsSharedWorker like CorsWorker and + * window.SharedWorker needs to be referenced during runtime only if it is supported (https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker) + * + * We guarantee the type assertion is correct by returning a SharedWorker in CorsSharedWorker constructor. + */ + createDetectorWorker() { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return new SharedWorker(new URL('./detector.worker', import.meta.url)) as globalThis.SharedWorker; }, reportCrash: async (report) => { diff --git a/public/app/core/utils/CorsSharedWorker.ts b/public/app/core/utils/CorsSharedWorker.ts new file mode 100644 index 00000000000..6edee786867 --- /dev/null +++ b/public/app/core/utils/CorsSharedWorker.ts @@ -0,0 +1,33 @@ +// Almost identical to CorsWorker.ts. Main difference being it allows loading a SharedWorker if browser supports it + +export function sharedWorkersSupported() { + return typeof window.SharedWorker !== 'undefined'; +} + +/** + * Creating CorsSharedWorker should be called only if sharedWorkersSupported() is truthy + */ +export class CorsSharedWorker { + constructor(url: URL, options?: WorkerOptions) { + if (!sharedWorkersSupported()) { + throw new Error('SharedWorker is not supported'); + } + // by default, worker inherits HTML document's location and pathname which leads to wrong public path value + // the CorsWorkerPlugin will override it with the value based on the initial worker chunk, ie. + // initial worker chunk: http://host.com/cdn/scripts/worker-123.js + // resulting public path: http://host.com/cdn/scripts + + const scriptUrl = url.toString(); + const scriptsBasePathUrl = new URL('.', url).toString(); + + const importScripts = `importScripts('${scriptUrl}');`; + const objectURL = URL.createObjectURL( + new Blob([`__webpack_worker_public_path__ = '${scriptsBasePathUrl}'; ${importScripts}`], { + type: 'application/javascript', + }) + ); + const worker = new SharedWorker(objectURL, options); + URL.revokeObjectURL(objectURL); + return worker; + } +} diff --git a/public/app/core/utils/CorsWorker.ts b/public/app/core/utils/CorsWorker.ts index 4cd2a140e0d..d8bf41d2aff 100644 --- a/public/app/core/utils/CorsWorker.ts +++ b/public/app/core/utils/CorsWorker.ts @@ -7,9 +7,7 @@ export class CorsWorker extends window.Worker { // resulting public path: http://host.com/cdn/scripts const scriptUrl = url.toString(); - const urlParts = scriptUrl.split('/'); - urlParts.pop(); - const scriptsBasePathUrl = `${urlParts.join('/')}/`; + const scriptsBasePathUrl = new URL('.', url).toString(); const importScripts = `importScripts('${scriptUrl}');`; const objectURL = URL.createObjectURL(