mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[FullStory] Filter custom events by an allowlist (#131148)
This commit is contained in:
parent
df225b213b
commit
13c928d4f3
10 changed files with 106 additions and 11 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
|
||||
import type { FullStoryApi } from './types';
|
||||
|
||||
/**
|
||||
* FullStory basic configuration.
|
||||
*/
|
||||
export interface FullStorySnippetConfig {
|
||||
/**
|
||||
* The FullStory account id.
|
||||
|
|
|
@ -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)',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue