Skip to content

Commit 6b971d6

Browse files
[Fullstory] add version and appId to reports (#111952) (#112212)
* add version and appId to reports * tests * code review * cr 2 * manual parsing + todo Co-authored-by: Liza Katz <[email protected]>
1 parent d9f477c commit 6b971d6

File tree

4 files changed

+97
-8
lines changed

4 files changed

+97
-8
lines changed

x-pack/plugins/cloud/public/fullstory.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ export interface FullStoryDeps {
1414
packageInfo: PackageInfo;
1515
}
1616

17+
export type FullstoryUserVars = Record<string, any>;
18+
1719
export interface FullStoryApi {
18-
identify(userId: string, userVars?: Record<string, any>): void;
20+
identify(userId: string, userVars?: FullstoryUserVars): void;
21+
setUserVars(userVars?: FullstoryUserVars): void;
1922
event(eventName: string, eventProperties: Record<string, any>): void;
2023
}
2124

x-pack/plugins/cloud/public/plugin.test.mocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { FullStoryDeps, FullStoryApi, FullStoryService } from './fullstory'
1010

1111
export const fullStoryApiMock: jest.Mocked<FullStoryApi> = {
1212
event: jest.fn(),
13+
setUserVars: jest.fn(),
1314
identify: jest.fn(),
1415
};
1516
export const initializeFullStoryMock = jest.fn<FullStoryService, [FullStoryDeps]>(() => ({

x-pack/plugins/cloud/public/plugin.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { homePluginMock } from 'src/plugins/home/public/mocks';
1111
import { securityMock } from '../../security/public/mocks';
1212
import { fullStoryApiMock, initializeFullStoryMock } from './plugin.test.mocks';
1313
import { CloudPlugin, CloudConfigType, loadFullStoryUserId } from './plugin';
14+
import { Observable, Subject } from 'rxjs';
1415

1516
describe('Cloud Plugin', () => {
1617
describe('#setup', () => {
@@ -23,10 +24,12 @@ describe('Cloud Plugin', () => {
2324
config = {},
2425
securityEnabled = true,
2526
currentUserProps = {},
27+
currentAppId$ = undefined,
2628
}: {
2729
config?: Partial<CloudConfigType>;
2830
securityEnabled?: boolean;
2931
currentUserProps?: Record<string, any>;
32+
currentAppId$?: Observable<string | undefined>;
3033
}) => {
3134
const initContext = coreMock.createPluginInitializerContext({
3235
id: 'cloudId',
@@ -39,9 +42,15 @@ describe('Cloud Plugin', () => {
3942
},
4043
...config,
4144
});
45+
4246
const plugin = new CloudPlugin(initContext);
4347

4448
const coreSetup = coreMock.createSetup();
49+
const coreStart = coreMock.createStart();
50+
if (currentAppId$) {
51+
coreStart.application.currentAppId$ = currentAppId$;
52+
}
53+
coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
4554
const securitySetup = securityMock.createSetup();
4655
securitySetup.authc.getCurrentUser.mockResolvedValue(
4756
securityMock.createMockAuthenticatedUser(currentUserProps)
@@ -78,10 +87,46 @@ describe('Cloud Plugin', () => {
7887
});
7988

8089
expect(fullStoryApiMock.identify).toHaveBeenCalledWith(
81-
'03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4'
90+
'03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4',
91+
{
92+
version_str: 'version',
93+
version_major_int: -1,
94+
version_minor_int: -1,
95+
version_patch_int: -1,
96+
}
8297
);
8398
});
8499

100+
it('calls FS.setUserVars everytime an app changes', async () => {
101+
const currentAppId$ = new Subject<string | undefined>();
102+
const { plugin } = await setupPlugin({
103+
config: { full_story: { enabled: true, org_id: 'foo' } },
104+
currentUserProps: {
105+
username: '1234',
106+
},
107+
currentAppId$,
108+
});
109+
110+
expect(fullStoryApiMock.setUserVars).not.toHaveBeenCalled();
111+
currentAppId$.next('App1');
112+
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
113+
app_id_str: 'App1',
114+
});
115+
currentAppId$.next();
116+
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
117+
app_id_str: 'unknown',
118+
});
119+
120+
currentAppId$.next('App2');
121+
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
122+
app_id_str: 'App2',
123+
});
124+
125+
expect(currentAppId$.observers.length).toBe(1);
126+
plugin.stop();
127+
expect(currentAppId$.observers.length).toBe(0);
128+
});
129+
85130
it('does not call FS.identify when security is not available', async () => {
86131
await setupPlugin({
87132
config: { full_story: { enabled: true, org_id: 'foo' } },

x-pack/plugins/cloud/public/plugin.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import {
1212
PluginInitializerContext,
1313
HttpStart,
1414
IBasePath,
15+
ApplicationStart,
1516
} from 'src/core/public';
1617
import { i18n } from '@kbn/i18n';
18+
import { Subscription } from 'rxjs';
1719
import type {
1820
AuthenticatedUser,
1921
SecurityPluginSetup,
@@ -57,17 +59,26 @@ export interface CloudSetup {
5759
isCloudEnabled: boolean;
5860
}
5961

62+
interface SetupFullstoryDeps extends CloudSetupDependencies {
63+
application?: Promise<ApplicationStart>;
64+
basePath: IBasePath;
65+
}
66+
6067
export class CloudPlugin implements Plugin<CloudSetup> {
6168
private config!: CloudConfigType;
6269
private isCloudEnabled: boolean;
70+
private appSubscription?: Subscription;
6371

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

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

144+
public stop() {
145+
this.appSubscription?.unsubscribe();
146+
}
147+
133148
/**
134149
* Determines if the current user should see links back to Cloud.
135150
* This isn't a true authorization check, but rather a heuristic to
@@ -156,10 +171,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
156171
return user?.roles.includes('superuser') ?? true;
157172
}
158173

159-
private async setupFullstory({
160-
basePath,
161-
security,
162-
}: CloudSetupDependencies & { basePath: IBasePath }) {
174+
private async setupFullstory({ basePath, security, application }: SetupFullstoryDeps) {
163175
const { enabled, org_id: orgId } = this.config.full_story;
164176
if (!enabled || !orgId) {
165177
return; // do not load any fullstory code in the browser if not enabled
@@ -190,7 +202,35 @@ export class CloudPlugin implements Plugin<CloudSetup> {
190202
if (userId) {
191203
// Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs
192204
const hashedId = sha256(userId.toString());
193-
fullStory.identify(hashedId);
205+
application
206+
?.then(async () => {
207+
const appStart = await application;
208+
this.appSubscription = appStart.currentAppId$.subscribe((appId) => {
209+
// Update the current application every time it changes
210+
fullStory.setUserVars({
211+
app_id_str: appId ?? 'unknown',
212+
});
213+
});
214+
})
215+
.catch((e) => {
216+
// eslint-disable-next-line no-console
217+
console.error(
218+
`[cloud.full_story] Could not retrieve application service due to error: ${e.toString()}`,
219+
e
220+
);
221+
});
222+
const kibanaVer = this.initializerContext.env.packageInfo.version;
223+
// TODO: use semver instead
224+
const parsedVer = (kibanaVer.indexOf('.') > -1 ? kibanaVer.split('.') : []).map((s) =>
225+
parseInt(s, 10)
226+
);
227+
// `str` suffix is required for evn vars, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234
228+
fullStory.identify(hashedId, {
229+
version_str: kibanaVer,
230+
version_major_int: parsedVer[0] ?? -1,
231+
version_minor_int: parsedVer[1] ?? -1,
232+
version_patch_int: parsedVer[2] ?? -1,
233+
});
194234
}
195235
} catch (e) {
196236
// eslint-disable-next-line no-console

0 commit comments

Comments
 (0)