Skip to content

Commit

Permalink
Improve unit test coverage of FS API calls (elastic#106242)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdover authored and kibanamachine committed Jul 21, 2021
1 parent e898416 commit fd8504a
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 38 deletions.
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';
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

0 comments on commit fd8504a

Please sign in to comment.