[FullStory] Demote the deployment information to setVars instead of setUserVars (#132837)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alejandro Fernández Haro 2022-06-01 16:08:22 +02:00 committed by GitHub
parent 848c831233
commit 0b190b9f16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 85 additions and 26 deletions

View file

@ -32,6 +32,10 @@ export interface EventContext {
* The Cloud ID.
*/
cloudId?: string;
/**
* `true` if the user is logged in via the Elastic Cloud authentication provider.
*/
isElasticCloudUser?: boolean;
/**
* The product's version.
*/

View file

@ -52,9 +52,27 @@ describe('FullStoryShipper', () => {
});
describe('FS.setUserVars', () => {
test('calls `setUserVars` when version is provided', () => {
fullstoryShipper.extendContext({ version: '1.2.3' });
test('calls `setUserVars` when isElasticCloudUser: true is provided', () => {
fullstoryShipper.extendContext({ isElasticCloudUser: true });
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
// eslint-disable-next-line @typescript-eslint/naming-convention
isElasticCloudUser_bool: true,
});
});
test('calls `setUserVars` when isElasticCloudUser: false is provided', () => {
fullstoryShipper.extendContext({ isElasticCloudUser: false });
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
// eslint-disable-next-line @typescript-eslint/naming-convention
isElasticCloudUser_bool: false,
});
});
});
describe('FS.setVars', () => {
test('calls `setVars` when version is provided', () => {
fullstoryShipper.extendContext({ version: '1.2.3' });
expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', {
version_str: '1.2.3',
version_major_int: 1,
version_minor_int: 2,
@ -62,14 +80,20 @@ describe('FullStoryShipper', () => {
});
});
test('calls `setUserVars` when cloudId is provided', () => {
test('calls `setVars` when cloudId is provided', () => {
fullstoryShipper.extendContext({ cloudId: 'test-es-org-id' });
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({ org_id_str: 'test-es-org-id' });
expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', {
// eslint-disable-next-line @typescript-eslint/naming-convention
cloudId_str: 'test-es-org-id',
org_id_str: 'test-es-org-id',
});
});
test('merges both: version and cloudId if both are provided', () => {
fullstoryShipper.extendContext({ version: '1.2.3', cloudId: 'test-es-org-id' });
expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({
expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', {
// eslint-disable-next-line @typescript-eslint/naming-convention
cloudId_str: 'test-es-org-id',
org_id_str: 'test-es-org-id',
version_str: '1.2.3',
version_major_int: 1,
@ -77,9 +101,7 @@ describe('FullStoryShipper', () => {
version_patch_int: 3,
});
});
});
describe('FS.setVars', () => {
test('adds the rest of the context to `setVars`', () => {
const context = {
userId: 'test-user-id',
@ -88,7 +110,16 @@ describe('FullStoryShipper', () => {
foo: 'bar',
};
fullstoryShipper.extendContext(context);
expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { foo_str: 'bar' });
expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', {
version_str: '1.2.3',
version_major_int: 1,
version_minor_int: 2,
version_patch_int: 3,
// eslint-disable-next-line @typescript-eslint/naming-convention
cloudId_str: 'test-es-org-id',
org_id_str: 'test-es-org-id',
foo_str: 'bar',
});
});
});
});

View file

@ -14,9 +14,9 @@ import type {
} from '@kbn/analytics-client';
import type { FullStoryApi } from './types';
import type { FullStorySnippetConfig } from './load_snippet';
import { getParsedVersion } from './get_parsed_version';
import { formatPayload } from './format_payload';
import { loadSnippet } from './load_snippet';
import { getParsedVersion } from './get_parsed_version';
/**
* FullStory shipper configuration.
@ -62,7 +62,7 @@ export class FullStoryShipper implements IShipper {
this.initContext.logger.debug(`Received context ${JSON.stringify(newContext)}`);
// FullStory requires different APIs for different type of contexts.
const { userId, version, cloudId, ...nonUserContext } = newContext;
const { userId, isElasticCloudUser, ...nonUserContext } = newContext;
// Call it only when the userId changes
if (userId && userId !== this.lastUserId) {
@ -73,14 +73,15 @@ export class FullStoryShipper implements IShipper {
}
// User-level context
if (version || cloudId) {
if (typeof isElasticCloudUser === 'boolean') {
this.initContext.logger.debug(
`Calling FS.setUserVars with version ${version} and cloudId ${cloudId}`
`Calling FS.setUserVars with isElasticCloudUser ${isElasticCloudUser}`
);
this.fullStoryApi.setUserVars(
formatPayload({
isElasticCloudUser,
})
);
this.fullStoryApi.setUserVars({
...(version ? getParsedVersion(version) : {}),
...(cloudId ? { org_id_str: cloudId } : {}),
});
}
// Event-level context. At the moment, only the scope `page` is supported by FullStory for webapps.
@ -88,11 +89,15 @@ export class FullStoryShipper implements IShipper {
// Keeping these fields for backwards compatibility.
if (nonUserContext.applicationId) nonUserContext.app_id = nonUserContext.applicationId;
if (nonUserContext.entityId) nonUserContext.ent_id = nonUserContext.entityId;
if (nonUserContext.cloudId) nonUserContext.org_id = nonUserContext.cloudId;
this.initContext.logger.debug(
`Calling FS.setVars with context ${JSON.stringify(nonUserContext)}`
);
this.fullStoryApi.setVars('page', formatPayload(nonUserContext));
this.fullStoryApi.setVars('page', {
...formatPayload(nonUserContext),
...(nonUserContext.version ? getParsedVersion(nonUserContext.version) : {}),
});
}
}

View file

@ -136,6 +136,7 @@ describe('Cloud Plugin', () => {
await expect(firstValueFrom(context$)).resolves.toEqual({
userId: '5ef112cfdae3dea57097bc276e275b2816e73ef2a398dc0ffaf5b6b4e3af2041',
isElasticCloudUser: false,
});
});
@ -150,7 +151,7 @@ describe('Cloud Plugin', () => {
([{ name }]) => name === 'cloud_user_id'
)!;
const hashId1 = await firstValueFrom(context1$);
const { userId: hashId1 } = (await firstValueFrom(context1$)) as { userId: string };
expect(hashId1).not.toEqual(expectedHashedPlainUsername);
const { coreSetup: coreSetup2 } = await setupPlugin({
@ -163,7 +164,7 @@ describe('Cloud Plugin', () => {
([{ name }]) => name === 'cloud_user_id'
)!;
const hashId2 = await firstValueFrom(context2$);
const { userId: hashId2 } = (await firstValueFrom(context2$)) as { userId: string };
expect(hashId2).not.toEqual(expectedHashedPlainUsername);
expect(hashId1).not.toEqual(hashId2);
@ -186,6 +187,7 @@ describe('Cloud Plugin', () => {
await expect(firstValueFrom(context$)).resolves.toEqual({
userId: expectedHashedPlainUsername,
isElasticCloudUser: true,
});
});
@ -203,6 +205,7 @@ describe('Cloud Plugin', () => {
await expect(firstValueFrom(context$)).resolves.toEqual({
userId: expectedHashedPlainUsername,
isElasticCloudUser: false,
});
});
@ -217,7 +220,10 @@ describe('Cloud Plugin', () => {
([{ name }]) => name === 'cloud_user_id'
)!;
await expect(firstValueFrom(context$)).resolves.toEqual({ userId: undefined });
await expect(firstValueFrom(context$)).resolves.toEqual({
userId: undefined,
isElasticCloudUser: false,
});
});
});

View file

@ -267,22 +267,35 @@ export class CloudPlugin implements Plugin<CloudSetup> {
user.authentication_realm?.type === 'saml' &&
user.authentication_realm?.name === 'cloud-saml-kibana'
) {
// If authenticated via Cloud SAML, use the SAML username as the user ID
return user.username;
// If the user is managed by ESS, use the plain username as the user ID:
// The username is expected to be unique for these users,
// and it matches how users are identified in the Cloud UI, so it allows us to correlate them.
return { userId: user.username, isElasticCloudUser: true };
}
return cloudId ? `${cloudId}:${user.username}` : user.username;
return {
// For the rest of the authentication providers, we want to add the cloud deployment ID to make it unique.
// Especially in the case of Elasticsearch-backed authentication, where users are commonly repeated
// across multiple deployments (i.e.: `elastic` superuser).
userId: cloudId ? `${cloudId}:${user.username}` : user.username,
isElasticCloudUser: false,
};
}),
// 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
map((userId) => ({ userId: sha256(userId) })),
catchError(() => of({ userId: undefined }))
map(({ userId, isElasticCloudUser }) => ({ userId: sha256(userId), isElasticCloudUser })),
catchError(() => of({ userId: undefined, isElasticCloudUser: false }))
),
schema: {
userId: {
type: 'keyword',
_meta: { description: 'The user id scoped as seen by Cloud (hashed)' },
},
isElasticCloudUser: {
type: 'boolean',
_meta: {
description: '`true` if the user is managed by ESS.',
},
},
},
});
}