Remove Gainsight from cloud plugin (#172318)

## Summary
- Remove gainsight plugin from cloud plugin
- add config deprecation in fullstory



Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cecilia Bollini 2024-01-09 10:21:56 +00:00 committed by GitHub
parent 8f9e11d001
commit de961a54a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 2 additions and 3628 deletions

View file

@ -29,7 +29,6 @@ snapshots.js
/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/**
/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/server/lib/pdf/assets/**
/x-pack/plugins/cloud_integrations/cloud_full_story/server/assets/**
/x-pack/plugins/cloud_integrations/cloud_gain_sight/server/assets/**
# package overrides
/packages/kbn-eslint-config

2
.github/CODEOWNERS vendored
View file

@ -39,7 +39,6 @@ packages/analytics/shippers/elastic_v3/browser @elastic/kibana-core
packages/analytics/shippers/elastic_v3/common @elastic/kibana-core
packages/analytics/shippers/elastic_v3/server @elastic/kibana-core
packages/analytics/shippers/fullstory @elastic/kibana-core
packages/analytics/shippers/gainsight @elastic/kibana-core
packages/kbn-apm-config-loader @elastic/kibana-core @vigneshshanmugam
x-pack/plugins/apm_data_access @elastic/obs-knowledge-team @elastic/obs-ux-infra_services-team
x-pack/plugins/apm @elastic/obs-ux-infra_services-team
@ -81,7 +80,6 @@ x-pack/plugins/cloud_integrations/cloud_data_migration @elastic/platform-onboard
x-pack/plugins/cloud_defend @elastic/kibana-cloud-security-posture
x-pack/plugins/cloud_integrations/cloud_experiments @elastic/kibana-core
x-pack/plugins/cloud_integrations/cloud_full_story @elastic/kibana-core
x-pack/plugins/cloud_integrations/cloud_gain_sight @elastic/kibana-core
x-pack/test/cloud_integration/plugins/saml_provider @elastic/kibana-core
x-pack/plugins/cloud_integrations/cloud_links @elastic/kibana-core
x-pack/plugins/cloud @elastic/kibana-core

View file

@ -225,11 +225,6 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
Portions of this code are licensed under the following license:
Gainsight PX Agent Wrapper Agent Version: 0.46.0 Installed: 2022-08-25 08:07
https://www.gainsight.com/policy/gainsight-px-license-agreement/
---
This code includes a copy of the `normalize-path`
https://github.com/jonschlinkert/normalize-path/blob/52c3a95ebebc2d98c1ad7606cbafa7e658656899/index.js
@ -347,9 +342,3 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
Copyright (c) 2013 HubSpot, Inc.
Released under the MIT license
https://github.com/HubSpot/vex/blob/master/LICENSE

View file

@ -502,10 +502,6 @@ The plugin exposes the static DefaultEditorController class to consume.
|Integrates with FullStory in order to provide better product analytics, so we can understand how our users make use of Kibana. This plugin should only run on Elastic Cloud.
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_gain_sight/README.md[cloudGainsight]
|Integrates with Gainsight in order to provide better product analytics, so we can understand how our users make use of Kibana. This plugin should only run on Elastic Cloud.
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_links/README.md[cloudLinks]
|Adds all the links to the Elastic Cloud console.

View file

@ -158,7 +158,6 @@
"@kbn/analytics-shippers-elastic-v3-common": "link:packages/analytics/shippers/elastic_v3/common",
"@kbn/analytics-shippers-elastic-v3-server": "link:packages/analytics/shippers/elastic_v3/server",
"@kbn/analytics-shippers-fullstory": "link:packages/analytics/shippers/fullstory",
"@kbn/analytics-shippers-gainsight": "link:packages/analytics/shippers/gainsight",
"@kbn/apm-config-loader": "link:packages/kbn-apm-config-loader",
"@kbn/apm-data-access-plugin": "link:x-pack/plugins/apm_data_access",
"@kbn/apm-plugin": "link:x-pack/plugins/apm",
@ -187,7 +186,6 @@
"@kbn/cloud-defend-plugin": "link:x-pack/plugins/cloud_defend",
"@kbn/cloud-experiments-plugin": "link:x-pack/plugins/cloud_integrations/cloud_experiments",
"@kbn/cloud-full-story-plugin": "link:x-pack/plugins/cloud_integrations/cloud_full_story",
"@kbn/cloud-gainsight-plugin": "link:x-pack/plugins/cloud_integrations/cloud_gain_sight",
"@kbn/cloud-integration-saml-provider-plugin": "link:x-pack/test/cloud_integration/plugins/saml_provider",
"@kbn/cloud-links-plugin": "link:x-pack/plugins/cloud_integrations/cloud_links",
"@kbn/cloud-plugin": "link:x-pack/plugins/cloud",

View file

@ -3,6 +3,5 @@
This directory holds the implementation of the _built-in_ shippers provided by the Analytics client. At the moment, the shippers are:
* [FullStory](./fullstory/README.md)
* [Gainsight](./gainsight/README.md)
* [Elastic V3 (Browser shipper)](./elastic_v3/browser/README.md)
* [Elastic V3 (Server-side shipper)](./elastic_v3/server/README.md)

View file

@ -1,14 +0,0 @@
# @kbn/analytics-shippers-gainsight
Gainsight implementation as a shipper for the `@kbn/analytics-client`.
## How to use it
This module is intended to be used **on the browser only**. It does not support server-side events.
```typescript
import { GainsightShipper } from "@kbn/analytics-shippers-gainsight";
analytics.registerShipper(GainsightShipper, { gainsightOrgId: '12345' })
```

View file

@ -1,10 +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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { GainsightShipper } from './src/gainsight_shipper';
export type { GainsightSnippetConfig } from './src/load_snippet';

View file

@ -1,13 +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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../../',
roots: ['<rootDir>/packages/analytics/shippers/gainsight'],
};

View file

@ -1,5 +0,0 @@
{
"type": "shared-browser",
"id": "@kbn/analytics-shippers-gainsight",
"owner": "@elastic/kibana-core"
}

View file

@ -1,7 +0,0 @@
{
"name": "@kbn/analytics-shippers-gainsight",
"private": true,
"version": "1.0.0",
"author": "Kibana Core",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -1,18 +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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { GainsightApi } from './types';
export const gainsightApiMock: GainsightApi = jest.fn();
gainsightApiMock.init = true;
jest.doMock('./load_snippet', () => {
return {
loadSnippet: () => gainsightApiMock,
};
});

View file

@ -1,102 +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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { loggerMock } from '@kbn/logging-mocks';
import { gainsightApiMock } from './gainsight_shipper.test.mocks';
import { GainsightShipper } from './gainsight_shipper';
describe('gainsightShipper', () => {
let gainsightShipper: GainsightShipper;
beforeEach(() => {
jest.resetAllMocks();
gainsightShipper = new GainsightShipper(
{
gainsightOrgId: 'test-org-id',
},
{
logger: loggerMock.create(),
sendTo: 'staging',
isDev: true,
}
);
});
describe('extendContext', () => {
describe('identify', () => {
test('calls `identify` when the clusterName is provided', () => {
const userId = 'test-user-id';
const clusterName = '123654';
gainsightShipper.extendContext({ userId, cluster_name: clusterName });
expect(gainsightApiMock).toHaveBeenCalledWith('identify', {
id: clusterName,
userType: 'deployment',
});
});
test('calls `identify` again only if the clusterName changes', () => {
const userId = 'test-user-id';
const clusterName = '123654';
gainsightShipper.extendContext({ userId, cluster_name: clusterName });
expect(gainsightApiMock).toHaveBeenCalledTimes(2);
expect(gainsightApiMock).toHaveBeenCalledWith('identify', {
id: clusterName,
userType: 'deployment',
});
expect(gainsightApiMock).toHaveBeenCalledWith('set', 'globalContext', {
kibanaUserId: userId,
});
gainsightShipper.extendContext({ userId, cluster_name: clusterName });
expect(gainsightApiMock).toHaveBeenCalledTimes(3);
gainsightShipper.extendContext({ userId, cluster_name: `${clusterName}-1` });
expect(gainsightApiMock).toHaveBeenCalledTimes(5); // called again because the user changed
});
});
});
describe('optIn', () => {
test('should call consent true and restart when isOptIn: true', () => {
gainsightShipper.optIn(true);
expect(gainsightApiMock).toHaveBeenCalledWith('config', 'enableTag', true);
});
test('should call consent false and shutdown when isOptIn: false', () => {
gainsightShipper.optIn(false);
expect(gainsightApiMock).toHaveBeenCalledWith('config', 'enableTag', false);
});
});
describe('reportEvents', () => {
test('calls the API once per event in the array with the properties transformed', () => {
gainsightShipper.reportEvents([
{
event_type: 'test-event-1',
timestamp: '2020-01-01T00:00:00.000Z',
properties: { test: 'test-1' },
context: { pageName: 'test-page-1' },
},
{
event_type: 'test-event-2',
timestamp: '2020-01-01T00:00:00.000Z',
properties: { other_property: 'test-2' },
context: { pageName: 'test-page-1' },
},
]);
expect(gainsightApiMock).toHaveBeenCalledTimes(2);
expect(gainsightApiMock).toHaveBeenCalledWith('track', 'test-event-1', {
test: 'test-1',
});
expect(gainsightApiMock).toHaveBeenCalledWith('track', 'test-event-2', {
other_property: 'test-2',
});
});
});
});

View file

@ -1,109 +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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type {
AnalyticsClientInitContext,
EventContext,
Event,
IShipper,
} from '@kbn/analytics-client';
import type { GainsightApi } from './types';
import type { GainsightSnippetConfig } from './load_snippet';
import { loadSnippet } from './load_snippet';
/**
* gainsight shipper.
*/
export class GainsightShipper implements IShipper {
/** Shipper's unique name */
public static shipperName = 'Gainsight';
private lastClusterName: string | undefined;
private readonly gainsightApi: GainsightApi;
/**
* Creates a new instance of the gainsightShipper.
* @param config {@link GainsightSnippetConfig}
* @param initContext {@link AnalyticsClientInitContext}
*/
constructor(
config: GainsightSnippetConfig,
private readonly initContext: AnalyticsClientInitContext
) {
const { ...snippetConfig } = config;
this.gainsightApi = loadSnippet(snippetConfig);
}
/**
* Calls track or set on the fields provided in the newContext.
* @param newContext The full new context to set {@link EventContext}
*/
public extendContext(newContext: EventContext): void {
this.initContext.logger.debug(`Received context ${JSON.stringify(newContext)}`);
// gainsight requires different APIs for different type of contexts.
const { userId, cluster_name: clusterName } = newContext;
this.gainsightApi('set', 'globalContext', {
kibanaUserId: userId,
});
if (clusterName && clusterName !== this.lastClusterName) {
this.initContext.logger.debug(`Calling identify with userId ${userId}`);
// We need to call the API for every new userId (restarting the session).
this.gainsightApi('identify', {
id: clusterName,
userType: 'deployment',
});
this.lastClusterName = clusterName;
} else {
this.initContext.logger.debug(
`Identify has already been called with ${userId} and ${clusterName}`
);
}
}
/**
* Stops/restarts the shipping mechanism based on the value of isOptedIn
* @param isOptedIn `true` for resume sending events. `false` to stop.
*/
public optIn(isOptedIn: boolean): void {
this.initContext.logger.debug(`Setting gainsight to optIn ${isOptedIn}`);
if (isOptedIn) {
this.gainsightApi('config', 'enableTag', true);
} else {
this.gainsightApi('config', 'enableTag', false);
}
}
/**
* Transforms the event into a valid format and calls `track`.
* @param events batched events {@link Event}
*/
public reportEvents(events: Event[]): void {
this.initContext.logger.debug(`Reporting ${events.length} events to gainsight`);
events.forEach((event) => {
// We only read event.properties and discard the rest because the context is already sent in the other APIs.
this.gainsightApi('track', event.event_type, event.properties);
});
}
/**
* Flushes all internal queues of the shipper.
* It doesn't really do anything inside because this shipper doesn't hold any internal queues.
*/
public async flush() {}
/**
* Shuts down the shipper.
* It doesn't really do anything inside because this shipper doesn't hold any internal queues.
*/
public shutdown() {
// No need to do anything here for now.
}
}

View file

@ -1,45 +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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { loadSnippet } from './load_snippet';
describe('loadSnippet', () => {
beforeAll(() => {
// Define necessary window and document global variables for the tests
Object.defineProperty(global, 'window', {
writable: true,
value: {
aptrinsic: {
init: true,
},
},
});
Object.defineProperty(global, 'document', {
writable: true,
value: {
createElement: jest.fn().mockReturnValue({}),
getElementsByTagName: jest
.fn()
.mockReturnValue([{ parentNode: { insertBefore: jest.fn() } }]),
},
});
Object.defineProperty(global, 'aptrinsic', {
writable: true,
value: {
init: true,
},
});
});
it('should return the gainsight API', () => {
const gainsightApi = loadSnippet({ gainsightOrgId: 'foo' });
expect(gainsightApi).toBeDefined();
});
});

View file

@ -1,64 +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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { GainsightApi } from './types';
/**
* gainsight basic configuration.
*/
export interface GainsightSnippetConfig {
/**
* The gainsight account id.
*/
gainsightOrgId: string;
/**
* The URL to load the gainsight client from. Falls back to `web-sdk.aptrinsic.com` if not specified.
*/
scriptUrl?: string;
cssFileEndpoint?: string;
widgetFileEndpoint?: string;
}
export function loadSnippet({
gainsightOrgId,
scriptUrl = 'web-sdk.aptrinsic.com/api/aptrinsic.js',
cssFileEndpoint = 'web-sdk.aptrinsic.com/style.css',
widgetFileEndpoint = 'web-sdk.aptrinsic.com/widget/aptrinsic-widget.js ',
}: GainsightSnippetConfig): GainsightApi {
/* eslint-disable no-var,dot-notation,prefer-rest-params,@typescript-eslint/no-unused-expressions */
(function (n, t, a, e, co) {
var i = 'aptrinsic';
// @ts-expect-error
(n[i] =
// @ts-expect-error
n[i] ||
function () {
// @ts-expect-error
(n[i].q = n[i].q || []).push(arguments);
}),
// @ts-expect-error
(n[i].p = e);
// @ts-expect-error
n[i].c = co;
var r = t.createElement('script');
(r.async = !1), (r.src = a + '?a=' + e);
var c = t.getElementsByTagName('script')[0];
// @ts-expect-error
c.parentNode.insertBefore(r, c);
})(window, document, scriptUrl, gainsightOrgId, {
cssFileEndpoint,
widgetFileEndpoint,
});
const gainsightApi = window['aptrinsic'];
if (!gainsightApi || !gainsightApi.init) {
throw new Error('Gainsight snippet failed to load. Check browser logs for more information.');
}
return gainsightApi;
}

View file

@ -1,29 +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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* Definition of the gainsight API.
*/
export interface GainsightApi {
init?: boolean;
(functionId: keyof Mapping, ...options: any): void;
}
interface Mapping {
identify: (id: string, userVars?: Record<string, unknown>) => void;
track: (event: string, data?: any) => void;
set: (event: string, data?: any) => void;
reset: () => void;
config: (options: any) => void;
}
declare global {
interface Window {
aptrinsic: GainsightApi;
}
}

View file

@ -1,20 +0,0 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts"
],
"kbn_references": [
"@kbn/analytics-client",
"@kbn/logging-mocks"
],
"exclude": [
"target/**/*",
]
}

View file

@ -18,7 +18,6 @@ pageLoadAssetSize:
cloudDefend: 18697
cloudExperiments: 59358
cloudFullStory: 18493
cloudGainsight: 18710
cloudLinks: 55984
cloudSecurityPosture: 19109
console: 46091

View file

@ -63,8 +63,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'--xpack.cloud_integrations.experiments.launch_darkly.client_id=a_string',
'--xpack.cloud_integrations.full_story.enabled=true',
'--xpack.cloud_integrations.full_story.org_id=a_string',
'--xpack.cloud_integrations.gain_sight.enabled=true',
'--xpack.cloud_integrations.gain_sight.org_id=a_string',
...findTestPluginPaths(path.resolve(__dirname, 'plugins')),
],
},

View file

@ -239,7 +239,6 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
// No PII. Just the list of event types we want to forward to FullStory.
'xpack.cloud_integrations.full_story.eventTypesAllowlist (array)',
'xpack.cloud_integrations.full_story.pageVarsDebounceTime (duration)',
'xpack.cloud_integrations.gain_sight.org_id (any)',
'xpack.cloud.id (string)',
'xpack.cloud.organization_url (string)',
'xpack.cloud.billing_url (string)',

View file

@ -72,8 +72,6 @@
"@kbn/analytics-shippers-elastic-v3-server/*": ["packages/analytics/shippers/elastic_v3/server/*"],
"@kbn/analytics-shippers-fullstory": ["packages/analytics/shippers/fullstory"],
"@kbn/analytics-shippers-fullstory/*": ["packages/analytics/shippers/fullstory/*"],
"@kbn/analytics-shippers-gainsight": ["packages/analytics/shippers/gainsight"],
"@kbn/analytics-shippers-gainsight/*": ["packages/analytics/shippers/gainsight/*"],
"@kbn/apm-config-loader": ["packages/kbn-apm-config-loader"],
"@kbn/apm-config-loader/*": ["packages/kbn-apm-config-loader/*"],
"@kbn/apm-data-access-plugin": ["x-pack/plugins/apm_data_access"],
@ -156,8 +154,6 @@
"@kbn/cloud-experiments-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_experiments/*"],
"@kbn/cloud-full-story-plugin": ["x-pack/plugins/cloud_integrations/cloud_full_story"],
"@kbn/cloud-full-story-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_full_story/*"],
"@kbn/cloud-gainsight-plugin": ["x-pack/plugins/cloud_integrations/cloud_gain_sight"],
"@kbn/cloud-gainsight-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_gain_sight/*"],
"@kbn/cloud-integration-saml-provider-plugin": ["x-pack/test/cloud_integration/plugins/saml_provider"],
"@kbn/cloud-integration-saml-provider-plugin/*": ["x-pack/test/cloud_integration/plugins/saml_provider/*"],
"@kbn/cloud-links-plugin": ["x-pack/plugins/cloud_integrations/cloud_links"],

View file

@ -64,6 +64,8 @@ export const config: PluginConfigDescriptor<CloudFullStoryConfigType> = {
{ path: 'xpack.cloud.full_story.enabled' },
{ path: 'xpack.cloud.full_story.org_id' },
{ path: 'xpack.cloud.full_story.eventTypesAllowlist' },
{ path: 'xpack.cloud_integrations.gain_sight.org_id' },
{ path: 'xpack.cloud_integrations.gain_sight.enabled' },
],
};
},

View file

@ -1,3 +0,0 @@
# Cloud Gainsight
Integrates with Gainsight in order to provide better product analytics, so we can understand how our users make use of Kibana. This plugin should only run on Elastic Cloud.

View file

@ -1,18 +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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../',
roots: ['<rootDir>/x-pack/plugins/cloud_integrations/cloud_gain_sight'],
coverageDirectory:
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/cloud_integrations/cloud_gain_sight',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/x-pack/plugins/cloud_integrations/cloud_gain_sight/{common,public,server}/**/*.{ts,tsx}',
],
};

View file

@ -1,19 +0,0 @@
{
"type": "plugin",
"id": "@kbn/cloud-gainsight-plugin",
"owner": "@elastic/kibana-core",
"description": "When Kibana runs on Elastic Cloud, this plugin registers Gainsight as a shipper for telemetry.",
"plugin": {
"id": "cloudGainsight",
"server": true,
"browser": true,
"configPath": [
"xpack",
"cloud_integrations",
"gain_sight"
],
"requiredPlugins": [
"cloud"
]
}
}

View file

@ -1,13 +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 type { PluginInitializerContext } from '@kbn/core/public';
import { CloudGainsightPlugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new CloudGainsightPlugin(initializerContext);
}

View file

@ -1,71 +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/public/mocks';
import type { CloudGainsightConfigType } from '../server/config';
import { CloudGainsightPlugin } from './plugin';
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
describe('Cloud Plugin', () => {
describe('#setup', () => {
describe('setupGainsight', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const setupPlugin = async ({
config = {},
isCloudEnabled = true,
}: {
config?: Partial<CloudGainsightConfigType>;
isCloudEnabled?: boolean;
}) => {
const initContext = coreMock.createPluginInitializerContext(config);
const plugin = new CloudGainsightPlugin(initContext);
const coreSetup = coreMock.createSetup();
const cloud = { ...cloudMock.createSetup(), isCloudEnabled };
plugin.setup(coreSetup, { cloud });
// Wait for Gainsight dynamic import to resolve
await new Promise((r) => setImmediate(r));
return { initContext, plugin, coreSetup };
};
test('register the shipper Gainsight with correct args when enabled and org_id are set', async () => {
const { coreSetup } = await setupPlugin({
config: { org_id: 'foo' },
});
expect(coreSetup.analytics.registerShipper).toHaveBeenCalled();
expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), {
gainsightOrgId: 'foo',
scriptUrl: '/internal/cloud/100/gainsight.js',
cssFileEndpoint: '/internal/cloud/100/gainsight.css',
widgetFileEndpoint: '/internal/cloud/100/gainsight_widget.js',
});
});
it('does not call initializeGainsight when isCloudEnabled=false', async () => {
const { coreSetup } = await setupPlugin({
config: { org_id: 'foo' },
isCloudEnabled: false,
});
expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled();
});
it('does not call initializeGainsight when org_id is undefined', async () => {
const { coreSetup } = await setupPlugin({ config: {} });
expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled();
});
});
});
});

View file

@ -1,78 +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 type {
AnalyticsServiceSetup,
IBasePath,
PluginInitializerContext,
CoreSetup,
Plugin,
} from '@kbn/core/public';
import type { CloudSetup } from '@kbn/cloud-plugin/public';
interface SetupGainsightDeps {
analytics: AnalyticsServiceSetup;
basePath: IBasePath;
}
interface CloudGainsightConfig {
org_id?: string;
}
interface CloudGainsightSetupDeps {
cloud: CloudSetup;
}
export class CloudGainsightPlugin implements Plugin {
private readonly config: CloudGainsightConfig;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get<CloudGainsightConfig>();
}
public setup(core: CoreSetup, { cloud }: CloudGainsightSetupDeps) {
if (cloud.isCloudEnabled) {
this.setupGainsight({ analytics: core.analytics, basePath: core.http.basePath }).catch((e) =>
// eslint-disable-next-line no-console
console.debug(`Error setting up Gainsight: ${e.toString()}`)
);
}
}
public start() {}
public stop() {}
/**
* If the right config is provided, register the Gainsight shipper to the analytics client.
* @param analytics Core's Analytics service's setup contract.
* @param basePath Core's http.basePath helper.
* @private
*/
private async setupGainsight({ analytics, basePath }: SetupGainsightDeps) {
const { org_id: gainsightOrgId } = this.config;
if (!gainsightOrgId) {
return; // do not load any Gainsight code in the browser if not enabled
}
// Keep this import async so that we do not load any Gainsight code into the browser when it is disabled.
const { GainsightShipper } = await import('@kbn/analytics-shippers-gainsight');
analytics.registerShipper(GainsightShipper, {
gainsightOrgId,
// Load an Elastic-internally audited script. Ideally, it should be hosted on a CDN.
scriptUrl: basePath.prepend(
`/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/gainsight.js`
),
cssFileEndpoint: basePath.prepend(
`/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/gainsight.css`
),
widgetFileEndpoint: basePath.prepend(
`/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/gainsight_widget.js`
),
});
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,31 +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 { config } from './config';
describe('xpack.cloud config', () => {
describe('gain_sight', () => {
it('allows org_id when enabled: false', () => {
expect(() => config.schema.validate({ enabled: false, org_id: 'asdf' })).not.toThrow();
});
it('rejects undefined or empty org_id when enabled: true', () => {
expect(() => config.schema.validate({ enabled: true })).toThrowErrorMatchingInlineSnapshot(
`"[org_id]: expected value of type [string] but got [undefined]"`
);
expect(() =>
config.schema.validate({ enabled: true, org_id: '' })
).toThrowErrorMatchingInlineSnapshot(
`"[org_id]: value has length [0] but it must have a minimum length of [1]."`
);
});
it('accepts org_id when enabled: true', () => {
expect(() => config.schema.validate({ enabled: true, org_id: 'asdf' })).not.toThrow();
});
});
});

View file

@ -1,28 +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 { schema, TypeOf } from '@kbn/config-schema';
import { PluginConfigDescriptor } from '@kbn/core/server';
const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
org_id: schema.conditional(
schema.siblingRef('enabled'),
true,
schema.string({ minLength: 1 }),
schema.maybe(schema.string())
),
});
export type CloudGainsightConfigType = TypeOf<typeof configSchema>;
export const config: PluginConfigDescriptor<CloudGainsightConfigType> = {
exposeToBrowser: {
org_id: true,
},
schema: configSchema,
};

View file

@ -1,15 +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 { PluginInitializerContext } from '@kbn/core/server';
export { config } from './config';
export async function plugin(initializerContext: PluginInitializerContext) {
const { CloudGainsightPlugin } = await import('./plugin');
return new CloudGainsightPlugin(initializerContext);
}

View file

@ -1,16 +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 registerGainsightRouteMock = jest.fn();
export const registerGainsightStyleRouteMock = jest.fn();
export const registerGainsightWidgetRouteMock = jest.fn();
jest.doMock('./routes', () => ({
registerGainsightRoute: registerGainsightRouteMock,
registerGainsightStyleRoute: registerGainsightStyleRouteMock,
registerGainsightWidgetRoute: registerGainsightWidgetRouteMock,
}));

View file

@ -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 { registerGainsightRouteMock } from './plugin.test.mock';
import { CloudGainsightPlugin } from './plugin';
describe('Cloud Gainsight plugin', () => {
let plugin: CloudGainsightPlugin;
beforeEach(() => {
registerGainsightRouteMock.mockReset();
plugin = new CloudGainsightPlugin(coreMock.createPluginInitializerContext());
});
test('registers route when cloud is enabled', () => {
plugin.setup(coreMock.createSetup(), {
cloud: { ...cloudMock.createSetup(), isCloudEnabled: true },
});
expect(registerGainsightRouteMock).toHaveBeenCalledTimes(1);
});
test('does not register the route when cloud is disabled', () => {
plugin.setup(coreMock.createSetup(), {
cloud: { ...cloudMock.createSetup(), isCloudEnabled: false },
});
expect(registerGainsightRouteMock).not.toHaveBeenCalled();
});
});

View file

@ -1,44 +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 type { PluginInitializerContext, CoreSetup, Plugin } from '@kbn/core/server';
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import {
registerGainsightRoute,
registerGainsightStyleRoute,
registerGainsightWidgetRoute,
} from './routes';
interface CloudGainsightSetupDeps {
cloud: CloudSetup;
}
export class CloudGainsightPlugin implements Plugin {
constructor(private readonly initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, { cloud }: CloudGainsightSetupDeps) {
if (cloud.isCloudEnabled) {
registerGainsightRoute({
httpResources: core.http.resources,
packageInfo: this.initializerContext.env.packageInfo,
});
registerGainsightWidgetRoute({
httpResources: core.http.resources,
packageInfo: this.initializerContext.env.packageInfo,
});
registerGainsightStyleRoute({
httpResources: core.http.resources,
packageInfo: this.initializerContext.env.packageInfo,
});
}
}
public start() {}
public stop() {}
}

View file

@ -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 { renderGainsightLibraryFactory, GAINSIGHT_LIBRARY_PATH } from './gainsight';
import fs from 'fs/promises';
const fsMock = fs as jest.Mocked<typeof fs>;
describe('renderGainsightLibraryFactory', () => {
beforeEach(() => {
jest.resetAllMocks();
fsMock.readFile.mockResolvedValue(Buffer.from('fake fs src'));
});
afterAll(() => jest.restoreAllMocks());
it('successfully returns file contents', async () => {
const render = renderGainsightLibraryFactory();
const { body } = await render();
expect(fsMock.readFile).toHaveBeenCalledWith(GAINSIGHT_LIBRARY_PATH);
expect(body.toString()).toEqual('fake fs src');
});
it('only reads from file system once callback is invoked', async () => {
const render = renderGainsightLibraryFactory();
expect(fsMock.readFile).not.toHaveBeenCalled();
await render();
expect(fsMock.readFile).toHaveBeenCalledTimes(1);
});
it('does not read from filesystem on subsequent calls', async () => {
const render = renderGainsightLibraryFactory();
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 = renderGainsightLibraryFactory(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 = renderGainsightLibraryFactory(false);
const { headers } = await render();
expect(headers).toEqual({
'cache-control': 'must-revalidate',
etag: '1e02f94b45750ba9284c111d31ae7e59c13b8e6e',
});
});
});

View file

@ -1,157 +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 GAINSIGHT_LIBRARY_PATH = path.join(__dirname, '..', 'assets', 'gainsight_library.js');
export const GAINSIGHT_WIDGET_PATH = path.join(__dirname, '..', 'assets', 'gainsight_widget.js');
export const GAINSIGHT_STYLE_PATH = path.join(__dirname, '..', 'assets', 'gainsight_style.css');
/** @internal exported for testing */
export const renderGainsightLibraryFactory = (dist = true, filePath = GAINSIGHT_LIBRARY_PATH) =>
once(
async (): Promise<{
body: Buffer;
headers: HttpResponseOptions['headers'];
}> => {
const srcBuffer = await fs.readFile(filePath);
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: calculateHash(srcBuffer) },
};
}
);
function calculateHash(srcBuffer: Buffer) {
const hash = createHash('sha1');
hash.update(srcBuffer);
return hash.digest('hex');
}
export const registerGainsightRoute = ({
httpResources,
packageInfo,
}: {
httpResources: HttpResources;
packageInfo: Readonly<PackageInfo>;
}) => {
const renderGainsightLibrary = renderGainsightLibraryFactory(
packageInfo.dist,
GAINSIGHT_LIBRARY_PATH
);
/**
* Register a custom JS endpoint in order to achieve 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}/gainsight.js`,
validate: false,
options: {
authRequired: false,
},
},
async (context, req, res) => {
try {
return res.renderJs(await renderGainsightLibrary());
} catch (e) {
return res.customError({
body: `Could not load Gainsight library from disk due to error: ${e.toString()}`,
statusCode: 500,
});
}
}
);
};
export const registerGainsightStyleRoute = ({
httpResources,
packageInfo,
}: {
httpResources: HttpResources;
packageInfo: Readonly<PackageInfo>;
}) => {
const renderGainsightLibrary = renderGainsightLibraryFactory(
packageInfo.dist,
GAINSIGHT_STYLE_PATH
);
/**
* Register a custom endpoint in order to achieve 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}/gainsight.css`,
validate: false,
options: {
authRequired: false,
},
},
async (context, req, res) => {
try {
return res.renderCss(await renderGainsightLibrary());
} catch (e) {
return res.customError({
body: `Could not load Gainsight library from disk due to error: ${e.toString()}`,
statusCode: 500,
});
}
}
);
};
export const registerGainsightWidgetRoute = ({
httpResources,
packageInfo,
}: {
httpResources: HttpResources;
packageInfo: Readonly<PackageInfo>;
}) => {
const renderGainsightLibrary = renderGainsightLibraryFactory(
packageInfo.dist,
GAINSIGHT_WIDGET_PATH
);
/**
* Register a custom JS endpoint in order to achieve 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}/gainsight_widget.js`,
validate: false,
options: {
authRequired: false,
},
},
async (context, req, res) => {
try {
return res.renderJs(await renderGainsightLibrary());
} catch (e) {
return res.customError({
body: `Could not load Gainsight library from disk due to error: ${e.toString()}`,
statusCode: 500,
});
}
}
);
};

View file

@ -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 {
registerGainsightRoute,
registerGainsightStyleRoute,
registerGainsightWidgetRoute,
} from './gainsight';

View file

@ -1,23 +0,0 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
},
"include": [
".storybook/**/*",
"common/**/*",
"public/**/*",
"server/**/*",
"../../../typings/**/*"
],
"exclude": [
"server/assets/*.js",
"target/**/*",
],
"kbn_references": [
"@kbn/core",
"@kbn/cloud-plugin",
"@kbn/config-schema",
"@kbn/analytics-shippers-gainsight",
]
}

View file

@ -3200,10 +3200,6 @@
version "0.0.0"
uid ""
"@kbn/analytics-shippers-gainsight@link:packages/analytics/shippers/gainsight":
version "0.0.0"
uid ""
"@kbn/analytics@link:packages/kbn-analytics":
version "0.0.0"
uid ""
@ -3368,10 +3364,6 @@
version "0.0.0"
uid ""
"@kbn/cloud-gainsight-plugin@link:x-pack/plugins/cloud_integrations/cloud_gain_sight":
version "0.0.0"
uid ""
"@kbn/cloud-integration-saml-provider-plugin@link:x-pack/test/cloud_integration/plugins/saml_provider":
version "0.0.0"
uid ""