diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f59a4..66153a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added - Anonymization of video recordings controllable from Gravity +- Option to enable/disable video recording anonymization in debug mode - Track new HTML events: - `contextmenu` - `dblclick` diff --git a/README.md b/README.md index 12bd4bd..42e143e 100644 --- a/README.md +++ b/README.md @@ -63,24 +63,25 @@ GravityCollector.init(/*options*/) The `GravityCollector.init()` can take a `CollectorOptions` object with the following optional properties: -| key | type | use | default value | -| ---------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | -| authKey | String | The authentication key provided by Gravity to select the correct collection | | -| requestInterval | Integer | Time (in ms) between two sends to Gravity server (buffering) | 1000 | -| gravityServerUrl | String | Gravity server URL | https://api.gravity.smartesting.com | -| debug | Boolean | Logs user action in the console instead of sending them to Gravity | false | -| maxDelay | Integer | In debug mode, adds a random delay (in ms) between 0 and this value before printing an user action. | 500 | -| excludeRegex | RegExp | Deprecated, use selectorsOptions instead.
Regular expression of ID and class names to ignore in selector computation. | null | -| customSelector | String (optional) | Deprecated, use selectorsOptions instead.
The attribute to use as a selector if defined on an HTML element targeted by a user action. | undefined | -| selectorsOptions | Object (optional) | See [Work with selectors](#work-with-selectors). | undefined | -| sessionsPercentageKept | [0..100] | Rate of sessions to be collected | 100 | -| rejectSession | function `() => boolean` | Boolean function to ignore session tracking. For instance, to ignore sessions from some bots:
rejectSession: () => /bot|googlebot|robot/i.test(navigator.userAgent) | `() => false` | -| onPublish | function (optional) | Adds a function called after each publish to the gravity server. | undefined | -| originsToRecord | String[] (optional) | Deprecated, renamed recordRequestsFor. | undefined | -| recordRequestsFor | String[] (optional) | The Gravity Data Collector does not record requests by default. You must specify here the URL origin(s) of the requests to record. For example: "https://myserver.com/" | undefined | -| buildId | String (optional) | The build reference when running tests | undefined | -| enableEventRecording | Boolean (optional) | Set to `false` to deactivate any recording (event & video) | true | -| enableVideoRecording | Boolean (optional) | Set to `false` to deactivate video recording | true | +| key | type | use | default value | +| ------------------------ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| authKey | String | The authentication key provided by Gravity to select the correct collection | | +| requestInterval | Integer | Time (in ms) between two sends to Gravity server (buffering) | 1000 | +| gravityServerUrl | String | Gravity server URL | https://api.gravity.smartesting.com | +| debug | Boolean | Logs user action in the console instead of sending them to Gravity | false | +| maxDelay | Integer | In debug mode, adds a random delay (in ms) between 0 and this value before printing an user action. | 500 | +| excludeRegex | RegExp | Deprecated, use selectorsOptions instead.
Regular expression of ID and class names to ignore in selector computation. | null | +| customSelector | String (optional) | Deprecated, use selectorsOptions instead.
The attribute to use as a selector if defined on an HTML element targeted by a user action. | undefined | +| selectorsOptions | Object (optional) | See [Work with selectors](#work-with-selectors). | undefined | +| sessionsPercentageKept | [0..100] | Rate of sessions to be collected | 100 | +| rejectSession | function `() => boolean` | Boolean function to ignore session tracking. For instance, to ignore sessions from some bots:
rejectSession: () => /bot|googlebot|robot/i.test(navigator.userAgent) | `() => false` | +| onPublish | function (optional) | Adds a function called after each publish to the gravity server. | undefined | +| originsToRecord | String[] (optional) | Deprecated, renamed recordRequestsFor. | undefined | +| recordRequestsFor | String[] (optional) | The Gravity Data Collector does not record requests by default. You must specify here the URL origin(s) of the requests to record. For example: "https://myserver.com/" | undefined | +| buildId | String (optional) | The build reference when running tests | undefined | +| enableEventRecording | Boolean (optional) | Set to `false` to deactivate any recording (event & video) (only in debug mode, otherwise this option is controlled from the Gravity interface) | true | +| enableVideoRecording | Boolean (optional) | Set to `false` to deactivate video recording (only in debug mode, otherwise this option is controlled from the Gravity interface) | true | +| enableVideoAnonymization | Boolean (optional) | Set to `false` to deactivate video anonymization (only in debug mode, otherwise this option is controlled from the Gravity interface) | true | ## Features diff --git a/src/collector/CollectorWrapper.ts b/src/collector/CollectorWrapper.ts index ee7dcab..a173adf 100644 --- a/src/collector/CollectorWrapper.ts +++ b/src/collector/CollectorWrapper.ts @@ -260,9 +260,9 @@ class CollectorWrapper { private async fetchRecordingSettings(): Promise { if (this.options.debug) { return { - enableEventRecording: true, - enableVideoRecording: true, - enableVideoAnonymization: false, + enableEventRecording: this.options.enableEventRecording, + enableVideoRecording: this.options.enableVideoRecording, + enableVideoAnonymization: this.options.enableVideoAnonymization, } } @@ -270,11 +270,10 @@ class CollectorWrapper { if (settings === null || error !== null) { return ALL_RECORDING_SETTINGS_DISABLED } - const enableEventRecording = settings.sessionRecording ?? this.options.enableEventRecording - const enableVideoRecording = settings.videoRecording ?? this.options.enableVideoRecording + return { - enableEventRecording, - enableVideoRecording, + enableEventRecording: settings.sessionRecording, + enableVideoRecording: settings.videoRecording, enableVideoAnonymization: settings.videoAnonymization, } }) diff --git a/src/collector/sessionRecording.test.ts b/src/collector/sessionRecording.test.ts index 3e0701d..b29fae1 100644 --- a/src/collector/sessionRecording.test.ts +++ b/src/collector/sessionRecording.test.ts @@ -65,7 +65,60 @@ describe('Session recording (events & video) depends on remote Gravity settings' beforeEach(() => installSpies(ConsoleGravityClient)) afterEach(uninstallSpies) - it('use default settings', async () => { + describe('use options settings if available', () => { + it('records events & video', async () => { + const collector = installer() + .withOptions({ + enableEventRecording: true, + enableVideoRecording: true, + }) + .install() + + await emitEachEventKind(collector) + expect(handleSessionUserActions).toHaveBeenCalled() + expect(handleSessionTraits).toHaveBeenCalled() + expect(handleScreenRecords).toHaveBeenCalled() + + expect(terminateEventRecording).not.toHaveBeenCalled() + expect(terminateVideoRecording).not.toHaveBeenCalled() + }) + + it('records events BUT no videos', async () => { + const collector = installer() + .withOptions({ + enableEventRecording: true, + enableVideoRecording: false, + }) + .install() + + await emitEachEventKind(collector) + expect(handleSessionUserActions).toHaveBeenCalled() + expect(handleSessionTraits).toHaveBeenCalled() + expect(handleScreenRecords).not.toHaveBeenCalled() + + expect(terminateEventRecording).not.toHaveBeenCalled() + expect(terminateVideoRecording).toHaveBeenCalled() + }) + + it('records nothing', async () => { + const collector = installer() + .withOptions({ + enableEventRecording: false, + enableVideoRecording: true, + }) + .install() + + await emitEachEventKind(collector) + expect(handleSessionUserActions).not.toHaveBeenCalled() + expect(handleSessionTraits).not.toHaveBeenCalled() + expect(handleScreenRecords).not.toHaveBeenCalled() + + expect(terminateEventRecording).toHaveBeenCalled() + expect(terminateVideoRecording).toHaveBeenCalled() + }) + }) + + it('else use default settings', async () => { const collector = installer().install() await emitEachEventKind(collector) diff --git a/src/gravity-client/AbstractGravityClient.ts b/src/gravity-client/AbstractGravityClient.ts index f2c86ff..b66930a 100644 --- a/src/gravity-client/AbstractGravityClient.ts +++ b/src/gravity-client/AbstractGravityClient.ts @@ -32,9 +32,10 @@ export interface GravityClientOptions { onPublish?: (userActions: ReadonlyArray) => void } -export type RecordingSettings = Pick & { - enableVideoAnonymization: boolean -} +export type RecordingSettings = Pick< + CollectorOptions, + 'enableEventRecording' | 'enableVideoRecording' | 'enableVideoAnonymization' +> export default abstract class AbstractGravityClient implements IGravityClient { private readonly sessionUserActionBuffer: DataBuffering diff --git a/src/types.ts b/src/types.ts index 2c1e6db..9e139d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -268,6 +268,7 @@ export interface CollectorOptions { disableVideoRecording?: boolean enableEventRecording?: boolean enableVideoRecording?: boolean + enableVideoAnonymization?: boolean } export type CollectorOptionsWithWindow = CollectorOptions & { diff --git a/src/utils/completeOptions.test.ts b/src/utils/completeOptions.test.ts index 88052fd..c8c2295 100644 --- a/src/utils/completeOptions.test.ts +++ b/src/utils/completeOptions.test.ts @@ -13,15 +13,24 @@ describe('completeOptions', () => { }) it('throws an error when provided an invalid "sessionsPercentageKept"', () => { - expect(() => completeOptions({ authKey: '123', sessionsPercentageKept: NaN })).toThrow( - 'option "sessionsPercentageKept": NaN is not a valid percentage (should be in range 0..100)', - ) - expect(() => completeOptions({ authKey: '123', sessionsPercentageKept: -1 })).toThrow( - 'option "sessionsPercentageKept": -1 is not a valid percentage (should be in range 0..100)', - ) - expect(() => completeOptions({ authKey: '123', sessionsPercentageKept: 101 })).toThrow( - 'option "sessionsPercentageKept": 101 is not a valid percentage (should be in range 0..100)', - ) + expect(() => + completeOptions({ + authKey: '123', + sessionsPercentageKept: NaN, + }), + ).toThrow('option "sessionsPercentageKept": NaN is not a valid percentage (should be in range 0..100)') + expect(() => + completeOptions({ + authKey: '123', + sessionsPercentageKept: -1, + }), + ).toThrow('option "sessionsPercentageKept": -1 is not a valid percentage (should be in range 0..100)') + expect(() => + completeOptions({ + authKey: '123', + sessionsPercentageKept: 101, + }), + ).toThrow('option "sessionsPercentageKept": 101 is not a valid percentage (should be in range 0..100)') }) describe('when selectorsOptions is set', () => { @@ -31,33 +40,50 @@ describe('completeOptions', () => { it('throws an error when selectorsOptions.attributes is not an array of string', () => { // @ts-expect-error selectorsOptions = { attributes: null } - expect(() => completeOptions({ authKey, selectorsOptions })).toThrow( - 'option "selectorsOptions.attributes": "null" is not a valid option. Expected a list of strings', - ) + expect(() => + completeOptions({ + authKey, + selectorsOptions, + }), + ).toThrow('option "selectorsOptions.attributes": "null" is not a valid option. Expected a list of strings') // @ts-expect-error selectorsOptions = { attributes: 'data-testid' } - expect(() => completeOptions({ authKey, selectorsOptions })).toThrow( - 'option "selectorsOptions.attributes": "data-testid" is not a valid option. Expected a list of strings', - ) + expect(() => + completeOptions({ + authKey, + selectorsOptions, + }), + ).toThrow('option "selectorsOptions.attributes": "data-testid" is not a valid option. Expected a list of strings') // @ts-expect-error selectorsOptions = { attributes: ['data-testid', null] } - expect(() => completeOptions({ authKey, selectorsOptions })).toThrow( - 'option "selectorsOptions.attributes": "null" is not a valid string', - ) + expect(() => + completeOptions({ + authKey, + selectorsOptions, + }), + ).toThrow('option "selectorsOptions.attributes": "null" is not a valid string') }) it('throws an error when selectorOptions.queries is not a list of QueryType', () => { // @ts-expect-error selectorsOptions = { queries: null } - expect(() => completeOptions({ authKey, selectorsOptions })).toThrow( - 'option "selectorsOptions.queries": "null" is not a valid option. Expected a list of QueryType', - ) + expect(() => + completeOptions({ + authKey, + selectorsOptions, + }), + ).toThrow('option "selectorsOptions.queries": "null" is not a valid option. Expected a list of QueryType') // @ts-expect-error selectorsOptions = { queries: [QueryType.id, 'doupidou'] } - expect(() => completeOptions({ authKey, selectorsOptions })).toThrow( + expect(() => + completeOptions({ + authKey, + selectorsOptions, + }), + ).toThrow( 'option "selectorsOptions.queries": "doupidou" is not a valid QueryType. Valid values are: id, class, tag', ) }) @@ -65,13 +91,21 @@ describe('completeOptions', () => { it('throws an error when selectorOptions.excludedQueries is not a list of QueryType', () => { // @ts-expect-error selectorsOptions = { excludedQueries: null } - expect(() => completeOptions({ authKey, selectorsOptions })).toThrow( - 'option "selectorsOptions.excludedQueries": "null" is not a valid option. Expected a list of QueryType', - ) + expect(() => + completeOptions({ + authKey, + selectorsOptions, + }), + ).toThrow('option "selectorsOptions.excludedQueries": "null" is not a valid option. Expected a list of QueryType') // @ts-expect-error selectorsOptions = { excludedQueries: [QueryType.id, 'doupidou'] } - expect(() => completeOptions({ authKey, selectorsOptions })).toThrow( + expect(() => + completeOptions({ + authKey, + selectorsOptions, + }), + ).toThrow( 'option "selectorsOptions.excludedQueries": "doupidou" is not a valid QueryType. Valid values are: id, class, tag', ) }) @@ -81,7 +115,10 @@ describe('completeOptions', () => { queries: [QueryType.class, QueryType.tag], attributes: ['role', 'data-testid'], } - completeOptions({ authKey, selectorsOptions }) + completeOptions({ + authKey, + selectorsOptions, + }) }) }) @@ -100,6 +137,7 @@ describe('completeOptions', () => { window, enableEventRecording: true, enableVideoRecording: true, + enableVideoAnonymization: true, } expect(completed).toStrictEqual(expected) }) @@ -124,6 +162,7 @@ describe('completeOptions', () => { window, enableEventRecording: true, enableVideoRecording: true, + enableVideoAnonymization: true, } expect(completed).toStrictEqual(expected) }) @@ -152,6 +191,7 @@ describe('completeOptions', () => { window, enableEventRecording: true, enableVideoRecording: true, + enableVideoAnonymization: true, } expect(completed).toStrictEqual(expected) }) @@ -174,6 +214,7 @@ describe('completeOptions', () => { window, enableEventRecording: true, enableVideoRecording: true, + enableVideoAnonymization: true, } expect(completed).toStrictEqual(expected) }) @@ -196,6 +237,7 @@ describe('completeOptions', () => { window, enableEventRecording: true, enableVideoRecording: true, + enableVideoAnonymization: true, } expect(completed).toStrictEqual(expected) }) @@ -218,6 +260,7 @@ describe('completeOptions', () => { window, enableEventRecording: true, enableVideoRecording: true, + enableVideoAnonymization: true, } expect(completed).toStrictEqual(expected) }) @@ -240,6 +283,7 @@ describe('completeOptions', () => { window, enableEventRecording: true, enableVideoRecording: true, + enableVideoAnonymization: true, } expect(completed).toStrictEqual(expected) }) @@ -247,16 +291,38 @@ describe('completeOptions', () => { }) it('use deprecated `disableVideoRecording` to set `enableVideoRecording`', () => { - expect(completeOptions({ authKey: '', disableVideoRecording: true })).toMatchObject({ + expect( + completeOptions({ + authKey: '', + disableVideoRecording: true, + }), + ).toMatchObject({ enableVideoRecording: false, }) - expect(completeOptions({ authKey: '', disableVideoRecording: false })).toMatchObject({ + expect( + completeOptions({ + authKey: '', + disableVideoRecording: false, + }), + ).toMatchObject({ enableVideoRecording: true, }) - expect(completeOptions({ authKey: '', disableVideoRecording: true, enableVideoRecording: true })).toMatchObject({ + expect( + completeOptions({ + authKey: '', + disableVideoRecording: true, + enableVideoRecording: true, + }), + ).toMatchObject({ enableVideoRecording: true, }) - expect(completeOptions({ authKey: '', disableVideoRecording: false, enableVideoRecording: false })).toMatchObject({ + expect( + completeOptions({ + authKey: '', + disableVideoRecording: false, + enableVideoRecording: false, + }), + ).toMatchObject({ enableVideoRecording: false, }) }) diff --git a/src/utils/completeOptions.ts b/src/utils/completeOptions.ts index c4df405..eaecd3b 100644 --- a/src/utils/completeOptions.ts +++ b/src/utils/completeOptions.ts @@ -22,6 +22,7 @@ export default function completeOptions(options?: Partial): Co rejectSession: DEFAULT_SESSION_REJECTION, enableEventRecording: true, enableVideoRecording: options.disableVideoRecording === undefined ? true : !options.disableVideoRecording, + enableVideoAnonymization: true, } const debugDefaultOptions = {