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/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/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/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
|
# package overrides
|
||||||
/packages/kbn-eslint-config
|
/packages/kbn-eslint-config
|
||||||
|
|
|
@ -31,6 +31,9 @@ describe('Cloud Plugin', () => {
|
||||||
const plugin = new CloudFullStoryPlugin(initContext);
|
const plugin = new CloudFullStoryPlugin(initContext);
|
||||||
|
|
||||||
const coreSetup = coreMock.createSetup();
|
const coreSetup = coreMock.createSetup();
|
||||||
|
jest
|
||||||
|
.spyOn(coreSetup.http.staticAssets, 'getPluginAssetHref')
|
||||||
|
.mockReturnValue('/cloudFullStory/assets/fs.js');
|
||||||
|
|
||||||
const cloud = { ...cloudMock.createSetup(), isCloudEnabled, isElasticStaffOwned };
|
const cloud = { ...cloudMock.createSetup(), isCloudEnabled, isElasticStaffOwned };
|
||||||
|
|
||||||
|
@ -50,7 +53,7 @@ describe('Cloud Plugin', () => {
|
||||||
expect(coreSetup.analytics.registerShipper).toHaveBeenCalled();
|
expect(coreSetup.analytics.registerShipper).toHaveBeenCalled();
|
||||||
expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), {
|
expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), {
|
||||||
fullStoryOrgId: 'foo',
|
fullStoryOrgId: 'foo',
|
||||||
scriptUrl: '/internal/cloud/100/fullstory.js',
|
scriptUrl: '/cloudFullStory/assets/fs.js',
|
||||||
namespace: 'FSKibana',
|
namespace: 'FSKibana',
|
||||||
captureOnStartup: false,
|
captureOnStartup: false,
|
||||||
});
|
});
|
||||||
|
@ -65,7 +68,7 @@ describe('Cloud Plugin', () => {
|
||||||
expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), {
|
expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), {
|
||||||
fullStoryOrgId: 'foo',
|
fullStoryOrgId: 'foo',
|
||||||
pageVarsDebounceTimeMs: 500,
|
pageVarsDebounceTimeMs: 500,
|
||||||
scriptUrl: '/internal/cloud/100/fullstory.js',
|
scriptUrl: '/cloudFullStory/assets/fs.js',
|
||||||
namespace: 'FSKibana',
|
namespace: 'FSKibana',
|
||||||
captureOnStartup: false,
|
captureOnStartup: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AnalyticsServiceSetup,
|
AnalyticsServiceSetup,
|
||||||
IBasePath,
|
IStaticAssets,
|
||||||
PluginInitializerContext,
|
PluginInitializerContext,
|
||||||
CoreSetup,
|
CoreSetup,
|
||||||
Plugin,
|
Plugin,
|
||||||
|
@ -17,7 +17,7 @@ import { duration } from 'moment';
|
||||||
|
|
||||||
interface SetupFullStoryDeps {
|
interface SetupFullStoryDeps {
|
||||||
analytics: AnalyticsServiceSetup;
|
analytics: AnalyticsServiceSetup;
|
||||||
basePath: IBasePath;
|
staticAssets: IStaticAssets;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CloudFullStoryConfig {
|
export interface CloudFullStoryConfig {
|
||||||
|
@ -45,10 +45,14 @@ export class CloudFullStoryPlugin implements Plugin {
|
||||||
.info('Skipping FullStory setup for a Elastic-owned deployments');
|
.info('Skipping FullStory setup for a Elastic-owned deployments');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setupFullStory({ analytics: core.analytics, basePath: core.http.basePath }).catch((e) =>
|
this.setupFullStory({
|
||||||
// eslint-disable-next-line no-console
|
analytics: core.analytics,
|
||||||
console.debug(`Error setting up FullStory: ${e.toString()}`)
|
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.
|
* If the right config is provided, register the FullStory shipper to the analytics client.
|
||||||
* @param analytics Core's Analytics service's setup contract.
|
* @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
|
||||||
*/
|
*/
|
||||||
private async setupFullStory({ analytics, basePath }: SetupFullStoryDeps) {
|
private async setupFullStory({ analytics, staticAssets }: SetupFullStoryDeps) {
|
||||||
const { org_id: fullStoryOrgId, eventTypesAllowlist, pageVarsDebounceTime } = this.config;
|
const { org_id: fullStoryOrgId, eventTypesAllowlist, pageVarsDebounceTime } = this.config;
|
||||||
if (!fullStoryOrgId) {
|
if (!fullStoryOrgId) {
|
||||||
return; // do not load any FullStory code in the browser if not enabled
|
return; // do not load any FullStory code in the browser if not enabled
|
||||||
|
@ -77,15 +81,9 @@ export class CloudFullStoryPlugin implements Plugin {
|
||||||
...(pageVarsDebounceTime
|
...(pageVarsDebounceTime
|
||||||
? { pageVarsDebounceTimeMs: duration(pageVarsDebounceTime).asMilliseconds() }
|
? { pageVarsDebounceTimeMs: duration(pageVarsDebounceTime).asMilliseconds() }
|
||||||
: {}),
|
: {}),
|
||||||
/**
|
scriptUrl: staticAssets.getPluginAssetHref('fs.js'),
|
||||||
* 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`
|
|
||||||
),
|
|
||||||
namespace: 'FSKibana',
|
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,
|
captureOnStartup: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,9 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginInitializerContext } from '@kbn/core/server';
|
|
||||||
|
|
||||||
export { config } from './config';
|
export { config } from './config';
|
||||||
|
|
||||||
export async function plugin(initializerContext: PluginInitializerContext) {
|
export async function plugin() {
|
||||||
const { CloudFullStoryPlugin } = await import('./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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { PluginInitializerContext, CoreSetup, Plugin } from '@kbn/core/server';
|
import type { Plugin } from '@kbn/core/server';
|
||||||
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
|
||||||
|
|
||||||
import { registerFullStoryRoute } from './routes';
|
|
||||||
|
|
||||||
interface CloudFullStorySetupDeps {
|
|
||||||
cloud: CloudSetup;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CloudFullStoryPlugin implements Plugin {
|
export class CloudFullStoryPlugin implements Plugin {
|
||||||
constructor(private readonly initializerContext: PluginInitializerContext) {}
|
public setup() {}
|
||||||
|
|
||||||
public setup(core: CoreSetup, { cloud }: CloudFullStorySetupDeps) {
|
|
||||||
if (cloud.isCloudEnabled) {
|
|
||||||
registerFullStoryRoute({
|
|
||||||
httpResources: core.http.resources,
|
|
||||||
packageInfo: this.initializerContext.env.packageInfo,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public start() {}
|
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