From 10d46e5f5e8d331fb61e83eb52494e47d28ca5b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 7 May 2025 23:06:24 +0200 Subject: [PATCH] chore(fullstory): serve the snippet as an asset (#220368) --- .eslintignore | 2 +- .../assets/fs.js} | 0 .../cloud_full_story/public/plugin.test.ts | 7 +- .../cloud_full_story/public/plugin.ts | 30 ++++---- .../cloud_full_story/server/index.ts | 6 +- .../server/plugin.test.mock.ts | 12 --- .../cloud_full_story/server/plugin.test.ts | 33 -------- .../cloud_full_story/server/plugin.ts | 20 +---- .../server/routes/fullstory.test.ts | 62 --------------- .../server/routes/fullstory.ts | 76 ------------------- .../cloud_full_story/server/routes/index.ts | 8 -- 11 files changed, 24 insertions(+), 232 deletions(-) rename x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/{server/assets/fullstory_library.js => public/assets/fs.js} (100%) delete mode 100644 x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.test.mock.ts delete mode 100644 x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.test.ts delete mode 100644 x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/fullstory.test.ts delete mode 100644 x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/fullstory.ts delete mode 100755 x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/index.ts diff --git a/.eslintignore b/.eslintignore index 1bf64cf91e42..d9d0695ab198 100644 --- a/.eslintignore +++ b/.eslintignore @@ -26,7 +26,7 @@ snapshots.js /x-pack/platform/plugins/private/canvas/storybook/build /x-pack/platform/plugins/private/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/** /x-pack/platform/plugins/private/reporting/server/export_types/printable_pdf_v2/server/lib/pdf/assets/** -/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/assets/** +/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/assets/** # package overrides /packages/kbn-eslint-config diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/assets/fullstory_library.js b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/assets/fs.js similarity index 100% rename from x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/assets/fullstory_library.js rename to x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/assets/fs.js diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/plugin.test.ts b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/plugin.test.ts index 8409a8275ef1..82a27b5fe46e 100644 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/plugin.test.ts +++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/plugin.test.ts @@ -31,6 +31,9 @@ describe('Cloud Plugin', () => { const plugin = new CloudFullStoryPlugin(initContext); const coreSetup = coreMock.createSetup(); + jest + .spyOn(coreSetup.http.staticAssets, 'getPluginAssetHref') + .mockReturnValue('/cloudFullStory/assets/fs.js'); const cloud = { ...cloudMock.createSetup(), isCloudEnabled, isElasticStaffOwned }; @@ -50,7 +53,7 @@ describe('Cloud Plugin', () => { expect(coreSetup.analytics.registerShipper).toHaveBeenCalled(); expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), { fullStoryOrgId: 'foo', - scriptUrl: '/internal/cloud/100/fullstory.js', + scriptUrl: '/cloudFullStory/assets/fs.js', namespace: 'FSKibana', captureOnStartup: false, }); @@ -65,7 +68,7 @@ describe('Cloud Plugin', () => { expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), { fullStoryOrgId: 'foo', pageVarsDebounceTimeMs: 500, - scriptUrl: '/internal/cloud/100/fullstory.js', + scriptUrl: '/cloudFullStory/assets/fs.js', namespace: 'FSKibana', captureOnStartup: false, }); diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/plugin.ts b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/plugin.ts index 05a635d782c0..2167ae639880 100755 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/plugin.ts +++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/public/plugin.ts @@ -7,7 +7,7 @@ import type { AnalyticsServiceSetup, - IBasePath, + IStaticAssets, PluginInitializerContext, CoreSetup, Plugin, @@ -17,7 +17,7 @@ import { duration } from 'moment'; interface SetupFullStoryDeps { analytics: AnalyticsServiceSetup; - basePath: IBasePath; + staticAssets: IStaticAssets; } export interface CloudFullStoryConfig { @@ -45,10 +45,14 @@ export class CloudFullStoryPlugin implements Plugin { .info('Skipping FullStory setup for a Elastic-owned deployments'); return; } - this.setupFullStory({ analytics: core.analytics, basePath: core.http.basePath }).catch((e) => - // eslint-disable-next-line no-console - console.debug(`Error setting up FullStory: ${e.toString()}`) - ); + this.setupFullStory({ + analytics: core.analytics, + staticAssets: core.http.staticAssets, + }).catch((e) => { + this.initializerContext.logger + .get() + .debug(`Error setting up FullStory: ${e.toString()}`, { error: e }); + }); } } @@ -59,10 +63,10 @@ export class CloudFullStoryPlugin implements Plugin { /** * If the right config is provided, register the FullStory shipper to the analytics client. * @param analytics Core's Analytics service's setup contract. - * @param basePath Core's http.basePath helper. + * @param staticAssets Core's http.staticAssets helper. * @private */ - private async setupFullStory({ analytics, basePath }: SetupFullStoryDeps) { + private async setupFullStory({ analytics, staticAssets }: SetupFullStoryDeps) { const { org_id: fullStoryOrgId, eventTypesAllowlist, pageVarsDebounceTime } = this.config; if (!fullStoryOrgId) { return; // do not load any FullStory code in the browser if not enabled @@ -77,15 +81,9 @@ export class CloudFullStoryPlugin implements Plugin { ...(pageVarsDebounceTime ? { pageVarsDebounceTimeMs: duration(pageVarsDebounceTime).asMilliseconds() } : {}), - /** - * FIXME: this should use the {@link IStaticAssets['getPluginAssetHref']} - * function. Then we can avoid registering our own endpoint in this plugin. - */ - scriptUrl: basePath.prepend( - `/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/fullstory.js` - ), + scriptUrl: staticAssets.getPluginAssetHref('fs.js'), namespace: 'FSKibana', - // Tell FullStory to not capture from the start, and wait for the opt-in confirmation + // Tell FullStory to not capture from the start and wait for the opt-in confirmation captureOnStartup: false, }); } diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/index.ts b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/index.ts index 55b049d66e26..6fad8ddfec72 100755 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/index.ts +++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/index.ts @@ -5,11 +5,9 @@ * 2.0. */ -import { PluginInitializerContext } from '@kbn/core/server'; - export { config } from './config'; -export async function plugin(initializerContext: PluginInitializerContext) { +export async function plugin() { const { CloudFullStoryPlugin } = await import('./plugin'); - return new CloudFullStoryPlugin(initializerContext); + return new CloudFullStoryPlugin(); } diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.test.mock.ts b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.test.mock.ts deleted file mode 100644 index c28c5e039d7d..000000000000 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.test.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const registerFullStoryRouteMock = jest.fn(); - -jest.doMock('./routes', () => ({ - registerFullStoryRoute: registerFullStoryRouteMock, -})); diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.test.ts b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.test.ts deleted file mode 100644 index 972437932bea..000000000000 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { coreMock } from '@kbn/core/server/mocks'; -import { cloudMock } from '@kbn/cloud-plugin/server/mocks'; -import { registerFullStoryRouteMock } from './plugin.test.mock'; -import { CloudFullStoryPlugin } from './plugin'; - -describe('Cloud FullStory plugin', () => { - let plugin: CloudFullStoryPlugin; - beforeEach(() => { - registerFullStoryRouteMock.mockReset(); - plugin = new CloudFullStoryPlugin(coreMock.createPluginInitializerContext()); - }); - - test('registers route when cloud is enabled', () => { - plugin.setup(coreMock.createSetup(), { - cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, - }); - expect(registerFullStoryRouteMock).toHaveBeenCalledTimes(1); - }); - - test('does not register the route when cloud is disabled', () => { - plugin.setup(coreMock.createSetup(), { - cloud: { ...cloudMock.createSetup(), isCloudEnabled: false }, - }); - expect(registerFullStoryRouteMock).not.toHaveBeenCalled(); - }); -}); diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.ts b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.ts index 52c3b32afa7f..8022c6801ed2 100755 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.ts +++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/plugin.ts @@ -5,26 +5,10 @@ * 2.0. */ -import type { PluginInitializerContext, CoreSetup, Plugin } from '@kbn/core/server'; -import type { CloudSetup } from '@kbn/cloud-plugin/server'; - -import { registerFullStoryRoute } from './routes'; - -interface CloudFullStorySetupDeps { - cloud: CloudSetup; -} +import type { Plugin } from '@kbn/core/server'; export class CloudFullStoryPlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} - - public setup(core: CoreSetup, { cloud }: CloudFullStorySetupDeps) { - if (cloud.isCloudEnabled) { - registerFullStoryRoute({ - httpResources: core.http.resources, - packageInfo: this.initializerContext.env.packageInfo, - }); - } - } + public setup() {} public start() {} diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/fullstory.test.ts b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/fullstory.test.ts deleted file mode 100644 index dae541a8c033..000000000000 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/fullstory.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -jest.mock('fs/promises'); -import { renderFullStoryLibraryFactory, FULLSTORY_LIBRARY_PATH } from './fullstory'; -import fs from 'fs/promises'; - -const fsMock = fs as jest.Mocked; - -describe('renderFullStoryLibraryFactory', () => { - beforeEach(() => { - jest.resetAllMocks(); - fsMock.readFile.mockResolvedValue(Buffer.from('fake fs src')); - }); - afterAll(() => jest.restoreAllMocks()); - - it('successfully returns file contents', async () => { - const render = renderFullStoryLibraryFactory(); - - const { body } = await render(); - expect(fsMock.readFile).toHaveBeenCalledWith(FULLSTORY_LIBRARY_PATH); - expect(body.toString()).toEqual('fake fs src'); - }); - - it('only reads from file system once callback is invoked', async () => { - const render = renderFullStoryLibraryFactory(); - expect(fsMock.readFile).not.toHaveBeenCalled(); - await render(); - expect(fsMock.readFile).toHaveBeenCalledTimes(1); - }); - - it('does not read from filesystem on subsequent calls', async () => { - const render = renderFullStoryLibraryFactory(); - await render(); - expect(fsMock.readFile).toHaveBeenCalledTimes(1); - await render(); - expect(fsMock.readFile).toHaveBeenCalledTimes(1); - await render(); - expect(fsMock.readFile).toHaveBeenCalledTimes(1); - }); - - it('returns max-age cache-control in dist', async () => { - const render = renderFullStoryLibraryFactory(true); - const { headers } = await render(); - expect(headers).toEqual({ - 'cache-control': 'max-age=31536000', - }); - }); - - it('returns must-revalidate cache-control and sha1 etag in dev', async () => { - const render = renderFullStoryLibraryFactory(false); - const { headers } = await render(); - expect(headers).toEqual({ - 'cache-control': 'must-revalidate', - etag: '1e02f94b45750ba9284c111d31ae7e59c13b8e6e', - }); - }); -}); diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/fullstory.ts b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/fullstory.ts deleted file mode 100644 index a33ad65d37d4..000000000000 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/fullstory.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import path from 'path'; -import fs from 'fs/promises'; -import { createHash } from 'crypto'; -import { once } from 'lodash'; -import { HttpResources, HttpResponseOptions, PackageInfo } from '@kbn/core/server'; - -const MINUTE = 60; -const HOUR = 60 * MINUTE; -const DAY = 24 * HOUR; - -/** @internal exported for testing */ -export const FULLSTORY_LIBRARY_PATH = path.join(__dirname, '..', 'assets', 'fullstory_library.js'); - -/** @internal exported for testing */ -export const renderFullStoryLibraryFactory = (dist = true) => - once( - async (): Promise<{ - body: Buffer; - headers: HttpResponseOptions['headers']; - }> => { - const srcBuffer = await fs.readFile(FULLSTORY_LIBRARY_PATH); - const hash = createHash('sha1'); // eslint-disable-line @kbn/eslint/no_unsafe_hash - hash.update(srcBuffer); - const hashDigest = hash.digest('hex'); - - return { - body: srcBuffer, - // In dist mode, return a long max-age, otherwise use etag + must-revalidate - headers: dist - ? { 'cache-control': `max-age=${DAY * 365}` } - : { 'cache-control': 'must-revalidate', etag: hashDigest }, - }; - } - ); - -export const registerFullStoryRoute = ({ - httpResources, - packageInfo, -}: { - httpResources: HttpResources; - packageInfo: Readonly; -}) => { - const renderFullStoryLibrary = renderFullStoryLibraryFactory(packageInfo.dist); - - /** - * Register a custom JS endpoint in order to acheive best caching possible with `max-age` similar to plugin bundles. - */ - httpResources.register( - { - // Use the build number in the URL path to leverage max-age caching on production builds - path: `/internal/cloud/${packageInfo.buildNum}/fullstory.js`, - validate: false, - options: { - authRequired: false, - }, - security: { authz: { enabled: false, reason: 'This route serves as a js endpoint' } }, - }, - async (context, req, res) => { - try { - return res.renderJs(await renderFullStoryLibrary()); - } catch (e) { - return res.customError({ - body: `Could not load FullStory library from disk due to error: ${e.toString()}`, - statusCode: 500, - }); - } - } - ); -}; diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/index.ts b/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/index.ts deleted file mode 100755 index 1ace8f41a795..000000000000 --- a/x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/routes/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { registerFullStoryRoute } from './fullstory';