Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve unit test coverage of FS API calls #106242

Merged
merged 3 commits into from
Jul 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 13 additions & 24 deletions x-pack/plugins/cloud/public/fullstory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,30 @@
* 2.0.
*/

import { sha256 } from 'js-sha256';
joshdover marked this conversation as resolved.
Show resolved Hide resolved
import { sha256 } from 'js-sha256'; // loaded here to reduce page load bundle size when FullStory is disabled
import type { IBasePath, PackageInfo } from '../../../../src/core/public';

export interface FullStoryDeps {
basePath: IBasePath;
orgId: string;
packageInfo: PackageInfo;
userId?: string;
}

interface FullStoryApi {
export interface FullStoryApi {
identify(userId: string, userVars?: Record<string, any>): void;
event(eventName: string, eventProperties: Record<string, any>): void;
}

export const initializeFullStory = async ({
export interface FullStoryService {
fullStory: FullStoryApi;
sha256: typeof sha256;
}

export const initializeFullStory = ({
basePath,
orgId,
packageInfo,
userId,
}: FullStoryDeps) => {
}: FullStoryDeps): FullStoryService => {
// @ts-expect-error
window._fs_debug = false;
// @ts-expect-error
Expand Down Expand Up @@ -75,22 +78,8 @@ export const initializeFullStory = async ({
// @ts-expect-error
const fullStory: FullStoryApi = window.FSKibana;

try {
// This needs to be called syncronously to be sure that we populate the user ID soon enough to make sessions merging
// across domains work
if (userId) {
// Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs
const hashedId = sha256(userId.toString());
fullStory.identify(hashedId);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(`[cloud.full_story] Could not call FS.identify due to error: ${e.toString()}`, e);
}

// Record an event that Kibana was opened so we can easily search for sessions that use Kibana
fullStory.event('Loaded Kibana', {
// `str` suffix is required, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234
kibana_version_str: packageInfo.version,
});
return {
fullStory,
sha256,
};
};
12 changes: 10 additions & 2 deletions x-pack/plugins/cloud/public/plugin.test.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@
* 2.0.
*/

import type { FullStoryDeps } from './fullstory';
import { sha256 } from 'js-sha256';
import type { FullStoryDeps, FullStoryApi, FullStoryService } from './fullstory';

export const initializeFullStoryMock = jest.fn<void, [FullStoryDeps]>();
export const fullStoryApiMock: jest.Mocked<FullStoryApi> = {
event: jest.fn(),
identify: jest.fn(),
};
export const initializeFullStoryMock = jest.fn<FullStoryService, [FullStoryDeps]>(() => ({
fullStory: fullStoryApiMock,
sha256,
}));
jest.doMock('./fullstory', () => {
return { initializeFullStory: initializeFullStoryMock };
});
67 changes: 58 additions & 9 deletions x-pack/plugins/cloud/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { nextTick } from '@kbn/test/jest';
import { coreMock } from 'src/core/public/mocks';
import { homePluginMock } from 'src/plugins/home/public/mocks';
import { securityMock } from '../../security/public/mocks';
import { initializeFullStoryMock } from './plugin.test.mocks';
import { fullStoryApiMock, initializeFullStoryMock } from './plugin.test.mocks';
import { CloudPlugin, CloudConfigType, loadFullStoryUserId } from './plugin';

describe('Cloud Plugin', () => {
describe('#setup', () => {
describe('setupFullstory', () => {
beforeEach(() => {
initializeFullStoryMock.mockReset();
jest.clearAllMocks();
});

const setupPlugin = async ({
Expand Down Expand Up @@ -63,23 +63,72 @@ describe('Cloud Plugin', () => {
});

expect(initializeFullStoryMock).toHaveBeenCalled();
const { basePath, orgId, packageInfo, userId } = initializeFullStoryMock.mock.calls[0][0];
const { basePath, orgId, packageInfo } = initializeFullStoryMock.mock.calls[0][0];
expect(basePath.prepend).toBeDefined();
expect(orgId).toEqual('foo');
expect(packageInfo).toEqual(initContext.env.packageInfo);
expect(userId).toEqual('1234');
});

it('passes undefined user ID when security is not available', async () => {
it('calls FS.identify with hashed user ID when security is available', async () => {
await setupPlugin({
config: { full_story: { enabled: true, org_id: 'foo' } },
currentUserProps: {
username: '1234',
},
});

expect(fullStoryApiMock.identify).toHaveBeenCalledWith(
'03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4'
);
});

it('does not call FS.identify when security is not available', async () => {
await setupPlugin({
config: { full_story: { enabled: true, org_id: 'foo' } },
securityEnabled: false,
});

expect(initializeFullStoryMock).toHaveBeenCalled();
const { orgId, userId } = initializeFullStoryMock.mock.calls[0][0];
expect(orgId).toEqual('foo');
expect(userId).toEqual(undefined);
expect(fullStoryApiMock.identify).not.toHaveBeenCalled();
});

it('calls FS.event when security is available', async () => {
const { initContext } = await setupPlugin({
config: { full_story: { enabled: true, org_id: 'foo' } },
currentUserProps: {
username: '1234',
},
});

expect(fullStoryApiMock.event).toHaveBeenCalledWith('Loaded Kibana', {
kibana_version_str: initContext.env.packageInfo.version,
});
});

it('calls FS.event when security is not available', async () => {
const { initContext } = await setupPlugin({
config: { full_story: { enabled: true, org_id: 'foo' } },
securityEnabled: false,
});

expect(fullStoryApiMock.event).toHaveBeenCalledWith('Loaded Kibana', {
kibana_version_str: initContext.env.packageInfo.version,
});
});

it('calls FS.event when FS.identify throws an error', async () => {
fullStoryApiMock.identify.mockImplementationOnce(() => {
throw new Error(`identify failed!`);
});
const { initContext } = await setupPlugin({
config: { full_story: { enabled: true, org_id: 'foo' } },
currentUserProps: {
username: '1234',
},
});

expect(fullStoryApiMock.event).toHaveBeenCalledWith('Loaded Kibana', {
kibana_version_str: initContext.env.packageInfo.version,
});
});

it('does not call initializeFullStory when enabled=false', async () => {
Expand Down
29 changes: 26 additions & 3 deletions x-pack/plugins/cloud/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
}: CloudSetupDependencies & { basePath: IBasePath }) {
const { enabled, org_id: orgId } = this.config.full_story;
if (!enabled || !orgId) {
return;
return; // do not load any fullstory code in the browser if not enabled
}

// Keep this import async so that we do not load any FullStory code into the browser when it is disabled.
Expand All @@ -171,16 +171,39 @@ export class CloudPlugin implements Plugin<CloudSetup> {
? loadFullStoryUserId({ getCurrentUser: security.authc.getCurrentUser })
: Promise.resolve(undefined);

// We need to call FS.identify synchronously after FullStory is initialized, so we must load the user upfront
const [{ initializeFullStory }, userId] = await Promise.all([
fullStoryChunkPromise,
userIdPromise,
]);

initializeFullStory({
const { fullStory, sha256 } = initializeFullStory({
basePath,
orgId,
packageInfo: this.initializerContext.env.packageInfo,
userId,
});

// Very defensive try/catch to avoid any UnhandledPromiseRejections
try {
// This needs to be called syncronously to be sure that we populate the user ID soon enough to make sessions merging
// across domains work
if (userId) {
// Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs
const hashedId = sha256(userId.toString());
fullStory.identify(hashedId);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(
`[cloud.full_story] Could not call FS.identify due to error: ${e.toString()}`,
e
);
}

// Record an event that Kibana was opened so we can easily search for sessions that use Kibana
fullStory.event('Loaded Kibana', {
// `str` suffix is required, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234
kibana_version_str: this.initializerContext.env.packageInfo.version,
});
}
}
Expand Down