mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
chore(fullstory): serve the snippet as an asset (#220368)
This commit is contained in:
parent
e269d04ee0
commit
10d46e5f5e
11 changed files with 24 additions and 232 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}));
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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() {}
|
||||
|
||||
|
|
|
@ -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<typeof fs>;
|
||||
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<PackageInfo>;
|
||||
}) => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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';
|
Loading…
Add table
Add a link
Reference in a new issue