mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[POC] Add Gainsight shipper for cloud (#141132)
* Add gainsight * Fix gainsight build * lint and prettier * tests * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * [CI] Auto-commit changed files from 'node scripts/generate codeowners' * fix tests * test * fix tests * move the configuration out of the cloud plugin 2 * tests * [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs' * pass deploymentId as user key * Add css and widget * lint * tests * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * tests * add render Css * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * fix gainSightApi * replace cluster id with cluster name * fix types and tests * Fix license * rename gainsight * remove formatpayload - remove formatPayload - remove typeDeps from shipper/g/kibana.jsonc - replace type shared-common with shared-browser * Add and fix tests - Add test for renderCss - update tests checking identify with different userId - remove optionalplugin security * add tests to gainsight shipper - test globalcontext request (set and remove) - hash only in dist ==false * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * move check on gainsight init * Remove translation update docs - update docs - always update userId * licence * fix tests Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ae4bda5465
commit
cea0d57cd9
48 changed files with 3818 additions and 0 deletions
|
@ -28,6 +28,7 @@ snapshots.js
|
|||
/x-pack/plugins/canvas/storybook/build
|
||||
/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_gain_sight/server/assets/**
|
||||
|
||||
# package overrides
|
||||
/packages/kbn-eslint-config
|
||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -688,6 +688,7 @@ 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/content-management/table_list @elastic/shared-ux
|
||||
packages/core/analytics/core-analytics-browser @elastic/kibana-core
|
||||
packages/core/analytics/core-analytics-browser-internal @elastic/kibana-core
|
||||
|
|
|
@ -253,6 +253,11 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||
Portions of this code are licensed under the following license:
|
||||
For license information please see https://edge.fullstory.com/s/fs.js.LICENSE.txt
|
||||
|
||||
---
|
||||
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
|
||||
|
|
|
@ -440,6 +440,10 @@ 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.
|
||||
|
||||
|
|
|
@ -140,6 +140,7 @@
|
|||
"@kbn/analytics-shippers-elastic-v3-common": "link:bazel-bin/packages/analytics/shippers/elastic_v3/common",
|
||||
"@kbn/analytics-shippers-elastic-v3-server": "link:bazel-bin/packages/analytics/shippers/elastic_v3/server",
|
||||
"@kbn/analytics-shippers-fullstory": "link:bazel-bin/packages/analytics/shippers/fullstory",
|
||||
"@kbn/analytics-shippers-gainsight": "link:bazel-bin/packages/analytics/shippers/gainsight",
|
||||
"@kbn/apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader",
|
||||
"@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils",
|
||||
"@kbn/cases-components": "link:bazel-bin/packages/kbn-cases-components",
|
||||
|
@ -865,6 +866,7 @@
|
|||
"@types/kbn__analytics-shippers-elastic-v3-common": "link:bazel-bin/packages/analytics/shippers/elastic_v3/common/npm_module_types",
|
||||
"@types/kbn__analytics-shippers-elastic-v3-server": "link:bazel-bin/packages/analytics/shippers/elastic_v3/server/npm_module_types",
|
||||
"@types/kbn__analytics-shippers-fullstory": "link:bazel-bin/packages/analytics/shippers/fullstory/npm_module_types",
|
||||
"@types/kbn__analytics-shippers-gainsight": "link:bazel-bin/packages/analytics/shippers/gainsight/npm_module_types",
|
||||
"@types/kbn__apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader/npm_module_types",
|
||||
"@types/kbn__apm-synthtrace": "link:bazel-bin/packages/kbn-apm-synthtrace/npm_module_types",
|
||||
"@types/kbn__apm-utils": "link:bazel-bin/packages/kbn-apm-utils/npm_module_types",
|
||||
|
|
|
@ -14,6 +14,7 @@ filegroup(
|
|||
"//packages/analytics/shippers/elastic_v3/common:build",
|
||||
"//packages/analytics/shippers/elastic_v3/server:build",
|
||||
"//packages/analytics/shippers/fullstory:build",
|
||||
"//packages/analytics/shippers/gainsight:build",
|
||||
"//packages/content-management/table_list:build",
|
||||
"//packages/core/analytics/core-analytics-browser:build",
|
||||
"//packages/core/analytics/core-analytics-browser-internal:build",
|
||||
|
@ -361,6 +362,7 @@ filegroup(
|
|||
"//packages/analytics/shippers/elastic_v3/common:build_types",
|
||||
"//packages/analytics/shippers/elastic_v3/server:build_types",
|
||||
"//packages/analytics/shippers/fullstory:build_types",
|
||||
"//packages/analytics/shippers/gainsight:build_types",
|
||||
"//packages/content-management/table_list:build_types",
|
||||
"//packages/core/analytics/core-analytics-browser:build_types",
|
||||
"//packages/core/analytics/core-analytics-browser-internal:build_types",
|
||||
|
|
135
packages/analytics/shippers/gainsight/BUILD.bazel
Normal file
135
packages/analytics/shippers/gainsight/BUILD.bazel
Normal file
|
@ -0,0 +1,135 @@
|
|||
load("@npm//@bazel/typescript:index.bzl", "ts_config")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
|
||||
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
|
||||
|
||||
PKG_DIRNAME = "gainsight"
|
||||
PKG_REQUIRE_NAME = "@kbn/analytics-shippers-gainsight"
|
||||
|
||||
SOURCE_FILES = glob(
|
||||
[
|
||||
"**/*.ts",
|
||||
],
|
||||
exclude = [
|
||||
"**/*.config.js",
|
||||
"**/*.mock.*",
|
||||
"**/*.test.*",
|
||||
"**/*.stories.*",
|
||||
"**/__snapshots__/**",
|
||||
"**/integration_tests/**",
|
||||
"**/mocks/**",
|
||||
"**/scripts/**",
|
||||
"**/storybook/**",
|
||||
"**/test_fixtures/**",
|
||||
"**/test_helpers/**",
|
||||
],
|
||||
)
|
||||
|
||||
SRCS = SOURCE_FILES
|
||||
|
||||
filegroup(
|
||||
name = "srcs",
|
||||
srcs = SRCS,
|
||||
)
|
||||
|
||||
NPM_MODULE_EXTRA_FILES = [
|
||||
"package.json",
|
||||
]
|
||||
|
||||
# In this array place runtime dependencies, including other packages and NPM packages
|
||||
# which must be available for this code to run.
|
||||
#
|
||||
# To reference other packages use:
|
||||
# "//repo/relative/path/to/package"
|
||||
# eg. "//packages/kbn-utils"
|
||||
#
|
||||
# To reference a NPM package use:
|
||||
# "@npm//name-of-package"
|
||||
# eg. "@npm//lodash"
|
||||
RUNTIME_DEPS = [
|
||||
"@npm//moment",
|
||||
]
|
||||
|
||||
# In this array place dependencies necessary to build the types, which will include the
|
||||
# :npm_module_types target of other packages and packages from NPM, including @types/*
|
||||
# packages.
|
||||
#
|
||||
# To reference the types for another package use:
|
||||
# "//repo/relative/path/to/package:npm_module_types"
|
||||
# eg. "//packages/kbn-utils:npm_module_types"
|
||||
#
|
||||
# References to NPM packages work the same as RUNTIME_DEPS
|
||||
TYPES_DEPS = [
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/jest",
|
||||
"@npm//moment",
|
||||
"//packages/analytics/client:npm_module_types",
|
||||
"//packages/kbn-logging-mocks:npm_module_types",
|
||||
]
|
||||
|
||||
jsts_transpiler(
|
||||
name = "target_node",
|
||||
srcs = SRCS,
|
||||
build_pkg_name = package_name(),
|
||||
)
|
||||
|
||||
jsts_transpiler(
|
||||
name = "target_web",
|
||||
srcs = SRCS,
|
||||
build_pkg_name = package_name(),
|
||||
web = True,
|
||||
)
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
deps = [
|
||||
"//:tsconfig.base.json",
|
||||
"//:tsconfig.bazel.json",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "tsc_types",
|
||||
args = ['--pretty'],
|
||||
srcs = SRCS,
|
||||
deps = TYPES_DEPS,
|
||||
declaration = True,
|
||||
declaration_map = True,
|
||||
emit_declaration_only = True,
|
||||
out_dir = "target_types",
|
||||
tsconfig = ":tsconfig",
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = PKG_DIRNAME,
|
||||
srcs = NPM_MODULE_EXTRA_FILES,
|
||||
deps = RUNTIME_DEPS + [":target_node", ":target_web"],
|
||||
package_name = PKG_REQUIRE_NAME,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_npm(
|
||||
name = "npm_module",
|
||||
deps = [":" + PKG_DIRNAME],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "build",
|
||||
srcs = [":npm_module"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_npm_types(
|
||||
name = "npm_module_types",
|
||||
srcs = SRCS,
|
||||
deps = [":tsc_types"],
|
||||
package_name = PKG_REQUIRE_NAME,
|
||||
tsconfig = ":tsconfig",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "build_types",
|
||||
srcs = [":npm_module_types"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
14
packages/analytics/shippers/gainsight/README.md
Normal file
14
packages/analytics/shippers/gainsight/README.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# @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' })
|
||||
```
|
||||
|
10
packages/analytics/shippers/gainsight/index.ts
Normal file
10
packages/analytics/shippers/gainsight/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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';
|
13
packages/analytics/shippers/gainsight/jest.config.js
Normal file
13
packages/analytics/shippers/gainsight/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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'],
|
||||
};
|
7
packages/analytics/shippers/gainsight/kibana.jsonc
Normal file
7
packages/analytics/shippers/gainsight/kibana.jsonc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/analytics-shippers-gainsight",
|
||||
"owner": "@elastic/kibana-core",
|
||||
"runtimeDeps": [],
|
||||
"typeDeps": []
|
||||
}
|
9
packages/analytics/shippers/gainsight/package.json
Normal file
9
packages/analytics/shippers/gainsight/package.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@kbn/analytics-shippers-gainsight",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"browser": "./target_web/index.js",
|
||||
"main": "./target_node/index.js",
|
||||
"author": "Kibana Core",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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,
|
||||
};
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
103
packages/analytics/shippers/gainsight/src/gainsight_shipper.ts
Normal file
103
packages/analytics/shippers/gainsight/src/gainsight_shipper.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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();
|
||||
});
|
||||
});
|
64
packages/analytics/shippers/gainsight/src/load_snippet.ts
Normal file
64
packages/analytics/shippers/gainsight/src/load_snippet.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
29
packages/analytics/shippers/gainsight/src/types.ts
Normal file
29
packages/analytics/shippers/gainsight/src/types.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
17
packages/analytics/shippers/gainsight/tsconfig.json
Normal file
17
packages/analytics/shippers/gainsight/tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.bazel.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "target_types",
|
||||
"stripInternal": false,
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
|
@ -251,6 +251,55 @@ describe('HttpResources service', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe('renderCss', () => {
|
||||
it('formats successful response', async () => {
|
||||
const cssBody = `body {border: 1px solid red;}`;
|
||||
register(routeConfig, async (ctx, req, res) => {
|
||||
return res.renderCss({ body: cssBody });
|
||||
});
|
||||
const [[, routeHandler]] = router.get.mock.calls;
|
||||
|
||||
const responseFactory = createHttpResourcesResponseFactory();
|
||||
await routeHandler(context, kibanaRequest, responseFactory);
|
||||
expect(responseFactory.ok).toHaveBeenCalledWith({
|
||||
body: cssBody,
|
||||
headers: {
|
||||
'content-type': 'text/css',
|
||||
'content-security-policy':
|
||||
"script-src 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can attach headers, except the CSP & "content-type" headers', async () => {
|
||||
const cssBody = `body {border: 1px solid red;}`;
|
||||
register(routeConfig, async (ctx, req, res) => {
|
||||
return res.renderCss({
|
||||
body: cssBody,
|
||||
headers: {
|
||||
'content-type': 'text/css5',
|
||||
'content-security-policy': "script-src 'unsafe-eval'",
|
||||
'x-kibana': '42',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const [[, routeHandler]] = router.get.mock.calls;
|
||||
|
||||
const responseFactory = createHttpResourcesResponseFactory();
|
||||
await routeHandler(context, kibanaRequest, responseFactory);
|
||||
|
||||
expect(responseFactory.ok).toHaveBeenCalledWith({
|
||||
body: cssBody,
|
||||
headers: {
|
||||
'content-type': 'text/css',
|
||||
'x-kibana': '42',
|
||||
'content-security-policy':
|
||||
"script-src 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -155,6 +155,16 @@ export class HttpResourcesService implements CoreService<InternalHttpResourcesSe
|
|||
},
|
||||
});
|
||||
},
|
||||
renderCss(options: HttpResourcesResponseOptions) {
|
||||
return response.ok({
|
||||
body: options.body,
|
||||
headers: {
|
||||
...options.headers,
|
||||
'content-type': 'text/css',
|
||||
'content-security-policy': cspHeader,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export function createHttpResourcesResponseFactory() {
|
|||
renderCoreApp: jest.fn(),
|
||||
renderAnonymousCoreApp: jest.fn(),
|
||||
renderHtml: jest.fn(),
|
||||
renderCss: jest.fn(),
|
||||
renderJs: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ function createHttpResourcesResponseFactory() {
|
|||
renderAnonymousCoreApp: jest.fn(),
|
||||
renderHtml: jest.fn(),
|
||||
renderJs: jest.fn(),
|
||||
renderCss: jest.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -53,6 +53,8 @@ export interface HttpResourcesServiceToolkit {
|
|||
renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse;
|
||||
/** To respond with a custom JS script file. */
|
||||
renderJs: (options: HttpResourcesResponseOptions) => IKibanaResponse;
|
||||
/** To respond with a custom CSS script file. */
|
||||
renderCss: (options: HttpResourcesResponseOptions) => IKibanaResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,7 @@ pageLoadAssetSize:
|
|||
cloudChat: 19894
|
||||
cloudExperiments: 59358
|
||||
cloudFullStory: 18493
|
||||
cloudGainsight: 18710
|
||||
cloudLinks: 17629
|
||||
cloudSecurityPosture: 19109
|
||||
console: 46091
|
||||
|
|
|
@ -68,6 +68,8 @@ 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',
|
||||
...plugins.map(
|
||||
(pluginDir) => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}`
|
||||
),
|
||||
|
|
|
@ -183,6 +183,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.cloud_integrations.full_story.org_id (any)',
|
||||
// No PII. Just the list of event types we want to forward to FullStory.
|
||||
'xpack.cloud_integrations.full_story.eventTypesAllowlist (array)',
|
||||
'xpack.cloud_integrations.gain_sight.org_id (any)',
|
||||
'xpack.cloud.id (string)',
|
||||
'xpack.cloud.organization_url (string)',
|
||||
'xpack.cloud.profile_url (string)',
|
||||
|
|
|
@ -321,6 +321,8 @@
|
|||
"@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-links-plugin": ["x-pack/plugins/cloud_integrations/cloud_links"],
|
||||
"@kbn/cloud-links-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_links/*"],
|
||||
"@kbn/cloud-security-posture-plugin": ["x-pack/plugins/cloud_security_posture"],
|
||||
|
|
3
x-pack/plugins/cloud_integrations/cloud_gain_sight/README.md
Executable file
3
x-pack/plugins/cloud_integrations/cloud_gain_sight/README.md
Executable file
|
@ -0,0 +1,3 @@
|
|||
# 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.
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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}',
|
||||
],
|
||||
};
|
15
x-pack/plugins/cloud_integrations/cloud_gain_sight/kibana.json
Executable file
15
x-pack/plugins/cloud_integrations/cloud_gain_sight/kibana.json
Executable file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"id": "cloudGainsight",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"owner": {
|
||||
"name": "Kibana Core",
|
||||
"githubTeam": "kibana-core"
|
||||
},
|
||||
"description": "When Kibana runs on Elastic Cloud, this plugin registers Gainsight as a shipper for telemetry.",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"configPath": ["xpack", "cloud_integrations", "gain_sight"],
|
||||
"requiredPlugins": ["cloud"],
|
||||
"optionalPlugins": []
|
||||
}
|
13
x-pack/plugins/cloud_integrations/cloud_gain_sight/public/index.ts
Executable file
13
x-pack/plugins/cloud_integrations/cloud_gain_sight/public/index.ts
Executable file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
78
x-pack/plugins/cloud_integrations/cloud_gain_sight/public/plugin.ts
Executable file
78
x-pack/plugins/cloud_integrations/cloud_gain_sight/public/plugin.ts
Executable file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 it is too large
Load diff
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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,
|
||||
};
|
15
x-pack/plugins/cloud_integrations/cloud_gain_sight/server/index.ts
Executable file
15
x-pack/plugins/cloud_integrations/cloud_gain_sight/server/index.ts
Executable file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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';
|
||||
import { CloudGainsightPlugin } from './plugin';
|
||||
|
||||
export { config } from './config';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new CloudGainsightPlugin(initializerContext);
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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,
|
||||
}));
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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();
|
||||
});
|
||||
});
|
44
x-pack/plugins/cloud_integrations/cloud_gain_sight/server/plugin.ts
Executable file
44
x-pack/plugins/cloud_integrations/cloud_gain_sight/server/plugin.ts
Executable file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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() {}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
12
x-pack/plugins/cloud_integrations/cloud_gain_sight/server/routes/index.ts
Executable file
12
x-pack/plugins/cloud_integrations/cloud_gain_sight/server/routes/index.ts
Executable file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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';
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
},
|
||||
"include": [
|
||||
".storybook/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../cloud/tsconfig.json" },
|
||||
]
|
||||
}
|
|
@ -2645,6 +2645,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/analytics-shippers-gainsight@link:bazel-bin/packages/analytics/shippers/gainsight":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/analytics@link:bazel-bin/packages/kbn-analytics":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -6821,6 +6825,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@types/kbn__analytics-shippers-gainsight@link:bazel-bin/packages/analytics/shippers/gainsight/npm_module_types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@types/kbn__analytics@link:bazel-bin/packages/kbn-analytics/npm_module_types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue