[FullStory] Filter custom events by an allowlist (#131148)

This commit is contained in:
Alejandro Fernández Haro 2022-05-04 13:08:25 +02:00 committed by GitHub
parent df225b213b
commit 13c928d4f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 106 additions and 11 deletions

View file

@ -176,7 +176,7 @@ export interface IAnalyticsClient {
) => void;
/**
* Registers the event type that will be emitted via the reportEvent API.
* @param eventTypeOps
* @param eventTypeOps The definition of the event type {@link EventTypeOpts}.
*/
registerEventType: <EventTypeData>(eventTypeOps: EventTypeOpts<EventTypeData>) => void;

View file

@ -36,7 +36,10 @@ export interface EventContext {
* The current entity ID (dashboard ID, visualization ID, etc.).
*/
entityId?: string;
// TODO: Extend with known keys
/**
* Additional keys are allowed.
*/
[key: string]: unknown;
}

View file

@ -25,7 +25,7 @@ export interface IShipper {
optIn: (isOptedIn: boolean) => void;
/**
* Perform any necessary calls to the persisting/analytics solution to set the event's context.
* @param newContext
* @param newContext The full new context to set {@link EventContext}
*/
extendContext?: (newContext: EventContext) => void;
/**

View file

@ -21,3 +21,7 @@ analytics.registerShipper(FullStoryShipper, { fullStoryOrgId: '12345' })
| `scriptUrl` | The URL to load the FullStory client from. Falls back to `edge.fullstory.com/s/fs.js` if not specified. |
| `debug` | Whether the debug logs should be printed to the console. Defaults to `false`. |
| `namespace` | The name of the variable where the API is stored: `window[namespace]`. Defaults to `FS`. |
## FullStory Custom Events Rate Limits
FullStory limits the number of custom events that can be sent per second ([docs](https://help.fullstory.com/hc/en-us/articles/360020623234#custom-property-rate-limiting)). In order to comply with that limit, this shipper will only emit the event types registered in the allow-list defined in the constant [CUSTOM_EVENT_TYPES_ALLOWLIST](./src/fullstory_shipper.ts). We may change this behaviour in the future to a remotely-controlled list of events or rely on the opt-in _cherry-pick_ config mechanism of the Analytics Client.

View file

@ -119,7 +119,7 @@ describe('FullStoryShipper', () => {
{
event_type: 'test-event-2',
timestamp: '2020-01-01T00:00:00.000Z',
properties: { test: 'test-2' },
properties: { other_property: 'test-2' },
context: { pageName: 'test-page-1' },
},
]);
@ -129,6 +129,49 @@ describe('FullStoryShipper', () => {
test_str: 'test-1',
});
expect(fullStoryApiMock.event).toHaveBeenCalledWith('test-event-2', {
other_property_str: 'test-2',
});
});
test('filters the events by the allow-list', () => {
fullstoryShipper = new FullStoryShipper(
{
eventTypesAllowlist: ['valid-event-1', 'valid-event-2'],
debug: true,
fullStoryOrgId: 'test-org-id',
},
{
logger: loggerMock.create(),
sendTo: 'staging',
isDev: true,
}
);
fullstoryShipper.reportEvents([
{
event_type: 'test-event-1', // Should be filtered out.
timestamp: '2020-01-01T00:00:00.000Z',
properties: { test: 'test-1' },
context: { pageName: 'test-page-1' },
},
{
event_type: 'valid-event-1',
timestamp: '2020-01-01T00:00:00.000Z',
properties: { test: 'test-1' },
context: { pageName: 'test-page-1' },
},
{
event_type: 'valid-event-2',
timestamp: '2020-01-01T00:00:00.000Z',
properties: { test: 'test-2' },
context: { pageName: 'test-page-1' },
},
]);
expect(fullStoryApiMock.event).toHaveBeenCalledTimes(2);
expect(fullStoryApiMock.event).toHaveBeenCalledWith('valid-event-1', {
test_str: 'test-1',
});
expect(fullStoryApiMock.event).toHaveBeenCalledWith('valid-event-2', {
test_str: 'test-2',
});
});

View file

@ -18,20 +18,46 @@ import { getParsedVersion } from './get_parsed_version';
import { formatPayload } from './format_payload';
import { loadSnippet } from './load_snippet';
export type FullStoryShipperConfig = FullStorySnippetConfig;
/**
* FullStory shipper configuration.
*/
export interface FullStoryShipperConfig extends FullStorySnippetConfig {
/**
* FullStory's custom events rate limit is very aggressive.
* If this setting is provided, it'll only send the event types specified in this list.
*/
eventTypesAllowlist?: string[];
}
/**
* FullStory shipper.
*/
export class FullStoryShipper implements IShipper {
/** Shipper's unique name */
public static shipperName = 'FullStory';
private readonly fullStoryApi: FullStoryApi;
private lastUserId: string | undefined;
private readonly eventTypesAllowlist?: string[];
/**
* Creates a new instance of the FullStoryShipper.
* @param config {@link FullStoryShipperConfig}
* @param initContext {@link AnalyticsClientInitContext}
*/
constructor(
config: FullStoryShipperConfig,
private readonly initContext: AnalyticsClientInitContext
) {
this.fullStoryApi = loadSnippet(config);
const { eventTypesAllowlist, ...snippetConfig } = config;
this.fullStoryApi = loadSnippet(snippetConfig);
this.eventTypesAllowlist = eventTypesAllowlist;
}
/**
* {@inheritDoc IShipper.extendContext}
* @param newContext {@inheritDoc IShipper.extendContext.newContext}
*/
public extendContext(newContext: EventContext): void {
this.initContext.logger.debug(`Received context ${JSON.stringify(newContext)}`);
@ -70,6 +96,10 @@ export class FullStoryShipper implements IShipper {
}
}
/**
* {@inheritDoc IShipper.optIn}
* @param isOptedIn {@inheritDoc IShipper.optIn.isOptedIn}
*/
public optIn(isOptedIn: boolean): void {
this.initContext.logger.debug(`Setting FS to optIn ${isOptedIn}`);
// FullStory uses 2 different opt-in methods:
@ -85,11 +115,17 @@ export class FullStoryShipper implements IShipper {
}
}
/**
* {@inheritDoc IShipper.reportEvents}
* @param events {@inheritDoc IShipper.reportEvents.events}
*/
public reportEvents(events: Event[]): void {
this.initContext.logger.debug(`Reporting ${events.length} events to FS`);
events.forEach((event) => {
// We only read event.properties and discard the rest because the context is already sent in the other APIs.
this.fullStoryApi.event(event.event_type, formatPayload(event.properties));
});
events
.filter((event) => this.eventTypesAllowlist?.includes(event.event_type) ?? true)
.forEach((event) => {
// We only read event.properties and discard the rest because the context is already sent in the other APIs.
this.fullStoryApi.event(event.event_type, formatPayload(event.properties));
});
}
}

View file

@ -8,6 +8,9 @@
import type { FullStoryApi } from './types';
/**
* FullStory basic configuration.
*/
export interface FullStorySnippetConfig {
/**
* The FullStory account id.

View file

@ -165,6 +165,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.cloud.deployment_url (string)',
'xpack.cloud.full_story.enabled (boolean)',
'xpack.cloud.full_story.org_id (any)',
'xpack.cloud.full_story.eventTypesAllowlist (array)',
'xpack.cloud.id (string)',
'xpack.cloud.organization_url (string)',
'xpack.cloud.profile_url (string)',

View file

@ -49,6 +49,7 @@ export interface CloudConfigType {
full_story: {
enabled: boolean;
org_id?: string;
eventTypesAllowlist?: string[];
};
/** Configuration to enable live chat in Cloud-enabled instances of Kibana. */
chat: {
@ -249,7 +250,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
* @private
*/
private async setupFullStory({ analytics, basePath }: SetupFullStoryDeps) {
const { enabled, org_id: fullStoryOrgId } = this.config.full_story;
const { enabled, org_id: fullStoryOrgId, eventTypesAllowlist } = this.config.full_story;
if (!enabled || !fullStoryOrgId) {
return; // do not load any FullStory code in the browser if not enabled
}
@ -257,6 +258,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
// Keep this import async so that we do not load any FullStory code into the browser when it is disabled.
const { FullStoryShipper } = await import('@kbn/analytics-shippers-fullstory');
analytics.registerShipper(FullStoryShipper, {
eventTypesAllowlist,
fullStoryOrgId,
// Load an Elastic-internally audited script. Ideally, it should be hosted on a CDN.
scriptUrl: basePath.prepend(

View file

@ -26,6 +26,9 @@ const fullStoryConfigSchema = schema.object({
schema.string({ minLength: 1 }),
schema.maybe(schema.string())
),
eventTypesAllowlist: schema.arrayOf(schema.string(), {
defaultValue: ['Loaded Kibana'],
}),
});
const chatConfigSchema = schema.object({