mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
Use execution context for Fullstory (#126780)
* fix a couple bugs in context management add execution context to fullstory * Update execution_context_service.ts * stop and app name tests * Use execution context in fullstory * Fix user hash Report org id to FS * Use setUserVars for esorgid * pass orgid into identify * fix Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
fd55bbde09
commit
ff90bd4d86
4 changed files with 121 additions and 37 deletions
|
@ -15,9 +15,11 @@ export interface FullStoryDeps {
|
|||
}
|
||||
|
||||
export type FullstoryUserVars = Record<string, any>;
|
||||
export type FullstoryVars = Record<string, any>;
|
||||
|
||||
export interface FullStoryApi {
|
||||
identify(userId: string, userVars?: FullstoryUserVars): void;
|
||||
setVars(pageName: string, vars?: FullstoryVars): void;
|
||||
setUserVars(userVars?: FullstoryUserVars): void;
|
||||
event(eventName: string, eventProperties: Record<string, any>): void;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { FullStoryDeps, FullStoryApi, FullStoryService } from './fullstory'
|
|||
export const fullStoryApiMock: jest.Mocked<FullStoryApi> = {
|
||||
event: jest.fn(),
|
||||
setUserVars: jest.fn(),
|
||||
setVars: jest.fn(),
|
||||
identify: jest.fn(),
|
||||
};
|
||||
export const initializeFullStoryMock = jest.fn<FullStoryService, [FullStoryDeps]>(() => ({
|
||||
|
|
|
@ -12,6 +12,7 @@ import { securityMock } from '../../security/public/mocks';
|
|||
import { fullStoryApiMock, initializeFullStoryMock } from './plugin.test.mocks';
|
||||
import { CloudPlugin, CloudConfigType, loadFullStoryUserId } from './plugin';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { KibanaExecutionContext } from 'kibana/public';
|
||||
|
||||
describe('Cloud Plugin', () => {
|
||||
describe('#setup', () => {
|
||||
|
@ -24,12 +25,12 @@ describe('Cloud Plugin', () => {
|
|||
config = {},
|
||||
securityEnabled = true,
|
||||
currentUserProps = {},
|
||||
currentAppId$ = undefined,
|
||||
currentContext$ = undefined,
|
||||
}: {
|
||||
config?: Partial<CloudConfigType>;
|
||||
securityEnabled?: boolean;
|
||||
currentUserProps?: Record<string, any>;
|
||||
currentAppId$?: Observable<string | undefined>;
|
||||
currentContext$?: Observable<KibanaExecutionContext>;
|
||||
}) => {
|
||||
const initContext = coreMock.createPluginInitializerContext({
|
||||
id: 'cloudId',
|
||||
|
@ -51,8 +52,8 @@ describe('Cloud Plugin', () => {
|
|||
const coreSetup = coreMock.createSetup();
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
if (currentAppId$) {
|
||||
coreStart.application.currentAppId$ = currentAppId$;
|
||||
if (currentContext$) {
|
||||
coreStart.executionContext.context$ = currentContext$;
|
||||
}
|
||||
|
||||
coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
|
||||
|
@ -94,44 +95,98 @@ describe('Cloud Plugin', () => {
|
|||
});
|
||||
|
||||
expect(fullStoryApiMock.identify).toHaveBeenCalledWith(
|
||||
'03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4',
|
||||
'5ef112cfdae3dea57097bc276e275b2816e73ef2a398dc0ffaf5b6b4e3af2041',
|
||||
{
|
||||
version_str: 'version',
|
||||
version_major_int: -1,
|
||||
version_minor_int: -1,
|
||||
version_patch_int: -1,
|
||||
org_id_str: 'cloudId',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('calls FS.setUserVars everytime an app changes', async () => {
|
||||
const currentAppId$ = new Subject<string | undefined>();
|
||||
it('user hash includes org id', async () => {
|
||||
await setupPlugin({
|
||||
config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg1' },
|
||||
currentUserProps: {
|
||||
username: '1234',
|
||||
},
|
||||
});
|
||||
|
||||
const hashId1 = fullStoryApiMock.identify.mock.calls[0][0];
|
||||
|
||||
await setupPlugin({
|
||||
config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg2' },
|
||||
currentUserProps: {
|
||||
username: '1234',
|
||||
},
|
||||
});
|
||||
|
||||
const hashId2 = fullStoryApiMock.identify.mock.calls[1][0];
|
||||
|
||||
expect(hashId1).not.toEqual(hashId2);
|
||||
});
|
||||
|
||||
it('calls FS.setVars everytime an app changes', async () => {
|
||||
const currentContext$ = new Subject<KibanaExecutionContext>();
|
||||
const { plugin } = await setupPlugin({
|
||||
config: { full_story: { enabled: true, org_id: 'foo' } },
|
||||
currentUserProps: {
|
||||
username: '1234',
|
||||
},
|
||||
currentAppId$,
|
||||
currentContext$,
|
||||
});
|
||||
|
||||
expect(fullStoryApiMock.setUserVars).not.toHaveBeenCalled();
|
||||
currentAppId$.next('App1');
|
||||
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
|
||||
// takes the app name
|
||||
expect(fullStoryApiMock.setVars).not.toHaveBeenCalled();
|
||||
currentContext$.next({
|
||||
name: 'App1',
|
||||
description: '123',
|
||||
});
|
||||
|
||||
expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', {
|
||||
pageName: 'App1',
|
||||
app_id_str: 'App1',
|
||||
});
|
||||
currentAppId$.next();
|
||||
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
|
||||
app_id_str: 'unknown',
|
||||
|
||||
// context clear
|
||||
currentContext$.next({});
|
||||
expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', {
|
||||
pageName: 'App1',
|
||||
app_id_str: 'App1',
|
||||
});
|
||||
|
||||
currentAppId$.next('App2');
|
||||
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
|
||||
// different app
|
||||
currentContext$.next({
|
||||
name: 'App2',
|
||||
page: 'page2',
|
||||
id: '123',
|
||||
});
|
||||
expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', {
|
||||
pageName: 'App2:page2',
|
||||
app_id_str: 'App2',
|
||||
page_str: 'page2',
|
||||
ent_id_str: '123',
|
||||
});
|
||||
|
||||
expect(currentAppId$.observers.length).toBe(1);
|
||||
// Back to first app
|
||||
currentContext$.next({
|
||||
name: 'App1',
|
||||
page: 'page3',
|
||||
id: '123',
|
||||
});
|
||||
|
||||
expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', {
|
||||
pageName: 'App1:page3',
|
||||
app_id_str: 'App1',
|
||||
page_str: 'page3',
|
||||
ent_id_str: '123',
|
||||
});
|
||||
|
||||
expect(currentContext$.observers.length).toBe(1);
|
||||
plugin.stop();
|
||||
expect(currentAppId$.observers.length).toBe(0);
|
||||
expect(currentContext$.observers.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not call FS.identify when security is not available', async () => {
|
||||
|
|
|
@ -13,11 +13,12 @@ import {
|
|||
PluginInitializerContext,
|
||||
HttpStart,
|
||||
IBasePath,
|
||||
ApplicationStart,
|
||||
ExecutionContextStart,
|
||||
} from 'src/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { compact, isUndefined, omitBy } from 'lodash';
|
||||
import type {
|
||||
AuthenticatedUser,
|
||||
SecurityPluginSetup,
|
||||
|
@ -83,8 +84,9 @@ export interface CloudSetup {
|
|||
}
|
||||
|
||||
interface SetupFullstoryDeps extends CloudSetupDependencies {
|
||||
application?: Promise<ApplicationStart>;
|
||||
executionContextPromise?: Promise<ExecutionContextStart>;
|
||||
basePath: IBasePath;
|
||||
esOrgId?: string;
|
||||
}
|
||||
|
||||
interface SetupChatDeps extends Pick<CloudSetupDependencies, 'security'> {
|
||||
|
@ -103,11 +105,16 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
}
|
||||
|
||||
public setup(core: CoreSetup, { home, security }: CloudSetupDependencies) {
|
||||
const application = core.getStartServices().then(([coreStart]) => {
|
||||
return coreStart.application;
|
||||
const executionContextPromise = core.getStartServices().then(([coreStart]) => {
|
||||
return coreStart.executionContext;
|
||||
});
|
||||
|
||||
this.setupFullstory({ basePath: core.http.basePath, security, application }).catch((e) =>
|
||||
this.setupFullstory({
|
||||
basePath: core.http.basePath,
|
||||
security,
|
||||
executionContextPromise,
|
||||
esOrgId: this.config.id,
|
||||
}).catch((e) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Error setting up FullStory: ${e.toString()}`)
|
||||
);
|
||||
|
@ -223,9 +230,14 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
return user?.roles.includes('superuser') ?? true;
|
||||
}
|
||||
|
||||
private async setupFullstory({ basePath, security, application }: SetupFullstoryDeps) {
|
||||
const { enabled, org_id: orgId } = this.config.full_story;
|
||||
if (!enabled || !orgId) {
|
||||
private async setupFullstory({
|
||||
basePath,
|
||||
security,
|
||||
executionContextPromise,
|
||||
esOrgId,
|
||||
}: SetupFullstoryDeps) {
|
||||
const { enabled, org_id: fsOrgId } = this.config.full_story;
|
||||
if (!enabled || !fsOrgId) {
|
||||
return; // do not load any fullstory code in the browser if not enabled
|
||||
}
|
||||
|
||||
|
@ -243,7 +255,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
|
||||
const { fullStory, sha256 } = initializeFullStory({
|
||||
basePath,
|
||||
orgId,
|
||||
orgId: fsOrgId,
|
||||
packageInfo: this.initializerContext.env.packageInfo,
|
||||
});
|
||||
|
||||
|
@ -252,16 +264,29 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
// 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());
|
||||
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',
|
||||
});
|
||||
// Join the cloud org id and the user to create a truly unique user id.
|
||||
// The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs
|
||||
const hashedId = sha256(esOrgId ? `${esOrgId}:${userId}` : `${userId}`);
|
||||
|
||||
executionContextPromise
|
||||
?.then(async (executionContext) => {
|
||||
this.appSubscription = executionContext.context$.subscribe((context) => {
|
||||
const { name, page, id } = context;
|
||||
// Update the current context every time it changes
|
||||
fullStory.setVars(
|
||||
'page',
|
||||
omitBy(
|
||||
{
|
||||
// Read about the special pageName property
|
||||
// https://help.fullstory.com/hc/en-us/articles/1500004101581-FS-setVars-API-Sending-custom-page-data-to-FullStory
|
||||
pageName: `${compact([name, page]).join(':')}`,
|
||||
app_id_str: name ?? 'unknown',
|
||||
page_str: page,
|
||||
ent_id_str: id,
|
||||
},
|
||||
isUndefined
|
||||
)
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -282,6 +307,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
version_major_int: parsedVer[0] ?? -1,
|
||||
version_minor_int: parsedVer[1] ?? -1,
|
||||
version_patch_int: parsedVer[2] ?? -1,
|
||||
org_id_str: esOrgId,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue