Skip to content

Commit

Permalink
[Fullstory] add version and appId to reports (#111952) (#112212)
Browse files Browse the repository at this point in the history
* add version and appId to reports

* tests

* code review

* cr 2

* manual parsing + todo

Co-authored-by: Liza Katz <[email protected]>
  • Loading branch information
kibanamachine and lizozom authored Sep 15, 2021
1 parent d9f477c commit 6b971d6
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 8 deletions.
5 changes: 4 additions & 1 deletion x-pack/plugins/cloud/public/fullstory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ export interface FullStoryDeps {
packageInfo: PackageInfo;
}

export type FullstoryUserVars = Record<string, any>;

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

Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/cloud/public/plugin.test.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { FullStoryDeps, FullStoryApi, FullStoryService } from './fullstory'

export const fullStoryApiMock: jest.Mocked<FullStoryApi> = {
event: jest.fn(),
setUserVars: jest.fn(),
identify: jest.fn(),
};
export const initializeFullStoryMock = jest.fn<FullStoryService, [FullStoryDeps]>(() => ({
Expand Down
47 changes: 46 additions & 1 deletion x-pack/plugins/cloud/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { homePluginMock } from 'src/plugins/home/public/mocks';
import { securityMock } from '../../security/public/mocks';
import { fullStoryApiMock, initializeFullStoryMock } from './plugin.test.mocks';
import { CloudPlugin, CloudConfigType, loadFullStoryUserId } from './plugin';
import { Observable, Subject } from 'rxjs';

describe('Cloud Plugin', () => {
describe('#setup', () => {
Expand All @@ -23,10 +24,12 @@ describe('Cloud Plugin', () => {
config = {},
securityEnabled = true,
currentUserProps = {},
currentAppId$ = undefined,
}: {
config?: Partial<CloudConfigType>;
securityEnabled?: boolean;
currentUserProps?: Record<string, any>;
currentAppId$?: Observable<string | undefined>;
}) => {
const initContext = coreMock.createPluginInitializerContext({
id: 'cloudId',
Expand All @@ -39,9 +42,15 @@ describe('Cloud Plugin', () => {
},
...config,
});

const plugin = new CloudPlugin(initContext);

const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
if (currentAppId$) {
coreStart.application.currentAppId$ = currentAppId$;
}
coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
const securitySetup = securityMock.createSetup();
securitySetup.authc.getCurrentUser.mockResolvedValue(
securityMock.createMockAuthenticatedUser(currentUserProps)
Expand Down Expand Up @@ -78,10 +87,46 @@ describe('Cloud Plugin', () => {
});

expect(fullStoryApiMock.identify).toHaveBeenCalledWith(
'03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4'
'03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4',
{
version_str: 'version',
version_major_int: -1,
version_minor_int: -1,
version_patch_int: -1,
}
);
});

it('calls FS.setUserVars everytime an app changes', async () => {
const currentAppId$ = new Subject<string | undefined>();
const { plugin } = await setupPlugin({
config: { full_story: { enabled: true, org_id: 'foo' } },
currentUserProps: {
username: '1234',
},
currentAppId$,
});

expect(fullStoryApiMock.setUserVars).not.toHaveBeenCalled();
currentAppId$.next('App1');
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
app_id_str: 'App1',
});
currentAppId$.next();
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
app_id_str: 'unknown',
});

currentAppId$.next('App2');
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
app_id_str: 'App2',
});

expect(currentAppId$.observers.length).toBe(1);
plugin.stop();
expect(currentAppId$.observers.length).toBe(0);
});

it('does not call FS.identify when security is not available', async () => {
await setupPlugin({
config: { full_story: { enabled: true, org_id: 'foo' } },
Expand Down
52 changes: 46 additions & 6 deletions x-pack/plugins/cloud/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import {
PluginInitializerContext,
HttpStart,
IBasePath,
ApplicationStart,
} from 'src/core/public';
import { i18n } from '@kbn/i18n';
import { Subscription } from 'rxjs';
import type {
AuthenticatedUser,
SecurityPluginSetup,
Expand Down Expand Up @@ -57,17 +59,26 @@ export interface CloudSetup {
isCloudEnabled: boolean;
}

interface SetupFullstoryDeps extends CloudSetupDependencies {
application?: Promise<ApplicationStart>;
basePath: IBasePath;
}

export class CloudPlugin implements Plugin<CloudSetup> {
private config!: CloudConfigType;
private isCloudEnabled: boolean;
private appSubscription?: Subscription;

constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get<CloudConfigType>();
this.isCloudEnabled = false;
}

public setup(core: CoreSetup, { home, security }: CloudSetupDependencies) {
this.setupFullstory({ basePath: core.http.basePath, security }).catch((e) =>
const application = core.getStartServices().then(([coreStart]) => {
return coreStart.application;
});
this.setupFullstory({ basePath: core.http.basePath, security, application }).catch((e) =>
// eslint-disable-next-line no-console
console.debug(`Error setting up FullStory: ${e.toString()}`)
);
Expand Down Expand Up @@ -130,6 +141,10 @@ export class CloudPlugin implements Plugin<CloudSetup> {
.catch(() => setLinks(true));
}

public stop() {
this.appSubscription?.unsubscribe();
}

/**
* Determines if the current user should see links back to Cloud.
* This isn't a true authorization check, but rather a heuristic to
Expand All @@ -156,10 +171,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
return user?.roles.includes('superuser') ?? true;
}

private async setupFullstory({
basePath,
security,
}: CloudSetupDependencies & { basePath: IBasePath }) {
private async setupFullstory({ basePath, security, application }: SetupFullstoryDeps) {
const { enabled, org_id: orgId } = this.config.full_story;
if (!enabled || !orgId) {
return; // do not load any fullstory code in the browser if not enabled
Expand Down Expand Up @@ -190,7 +202,35 @@ export class CloudPlugin implements Plugin<CloudSetup> {
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);
application
?.then(async () => {
const appStart = await application;
this.appSubscription = appStart.currentAppId$.subscribe((appId) => {
// Update the current application every time it changes
fullStory.setUserVars({
app_id_str: appId ?? 'unknown',
});
});
})
.catch((e) => {
// eslint-disable-next-line no-console
console.error(
`[cloud.full_story] Could not retrieve application service due to error: ${e.toString()}`,
e
);
});
const kibanaVer = this.initializerContext.env.packageInfo.version;
// TODO: use semver instead
const parsedVer = (kibanaVer.indexOf('.') > -1 ? kibanaVer.split('.') : []).map((s) =>
parseInt(s, 10)
);
// `str` suffix is required for evn vars, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234
fullStory.identify(hashedId, {
version_str: kibanaVer,
version_major_int: parsedVer[0] ?? -1,
version_minor_int: parsedVer[1] ?? -1,
version_patch_int: parsedVer[2] ?? -1,
});
}
} catch (e) {
// eslint-disable-next-line no-console
Expand Down

0 comments on commit 6b971d6

Please sign in to comment.