Skip to content

ref(replay): Extract integration to clarify public API #6457

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 1 addition & 195 deletions packages/replay/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,195 +1 @@
import type { BrowserClient, BrowserOptions } from '@sentry/browser';
import { getCurrentHub } from '@sentry/core';
import { Integration } from '@sentry/types';

import { DEFAULT_ERROR_SAMPLE_RATE, DEFAULT_SESSION_SAMPLE_RATE } from './constants';
import { ReplayContainer } from './replay';
import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions } from './types';
import { captureInternalException } from './util/captureInternalException';
import { isBrowser } from './util/isBrowser';

const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed,map,audio';

let _initialized = false;

export class Replay implements Integration {
/**
* @inheritDoc
*/
public static id: string = 'Replay';

/**
* @inheritDoc
*/
public name: string = Replay.id;

/**
* Options to pass to `rrweb.record()`
*/
readonly recordingOptions: RecordingOptions;

readonly options: ReplayPluginOptions;

/** In tests, this is only called the first time */
protected _hasCalledSetupOnce: boolean = false;

private _replay?: ReplayContainer;

constructor({
flushMinDelay = 5000,
flushMaxDelay = 15000,
initialFlushDelay = 5000,
stickySession = true,
useCompression = true,
sessionSampleRate,
errorSampleRate,
maskAllText = true,
maskAllInputs = true,
blockAllMedia = true,
blockClass = 'sentry-block',
ignoreClass = 'sentry-ignore',
maskTextClass = 'sentry-mask',
blockSelector = '[data-sentry-block]',
...recordingOptions
}: ReplayConfiguration = {}) {
this.recordingOptions = {
maskAllInputs,
blockClass,
ignoreClass,
maskTextClass,
blockSelector,
...recordingOptions,
};

this.options = {
flushMinDelay,
flushMaxDelay,
stickySession,
initialFlushDelay,
sessionSampleRate: DEFAULT_SESSION_SAMPLE_RATE,
errorSampleRate: DEFAULT_ERROR_SAMPLE_RATE,
useCompression,
maskAllText,
blockAllMedia,
};

if (typeof sessionSampleRate === 'number') {
// eslint-disable-next-line
console.warn(
`[Replay] You are passing \`sessionSampleRate\` to the Replay integration.
This option is deprecated and will be removed soon.
Instead, configure \`replaysSessionSampleRate\` directly in the SDK init options, e.g.:
Sentry.init({ replaysSessionSampleRate: ${sessionSampleRate} })`,
);

this.options.sessionSampleRate = sessionSampleRate;
}

if (typeof errorSampleRate === 'number') {
// eslint-disable-next-line
console.warn(
`[Replay] You are passing \`errorSampleRate\` to the Replay integration.
This option is deprecated and will be removed soon.
Instead, configure \`replaysOnErrorSampleRate\` directly in the SDK init options, e.g.:
Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
);

this.options.errorSampleRate = errorSampleRate;
}

if (this.options.maskAllText) {
// `maskAllText` is a more user friendly option to configure
// `maskTextSelector`. This means that all nodes will have their text
// content masked.
this.recordingOptions.maskTextSelector = '*';
}

if (this.options.blockAllMedia) {
// `blockAllMedia` is a more user friendly option to configure blocking
// embedded media elements
this.recordingOptions.blockSelector = !this.recordingOptions.blockSelector
? MEDIA_SELECTORS
: `${this.recordingOptions.blockSelector},${MEDIA_SELECTORS}`;
}

if (isBrowser() && _initialized) {
const error = new Error('Multiple Sentry Session Replay instances are not supported');
captureInternalException(error);
throw error;
}

_initialized = true;
}

/**
* We previously used to create a transaction in `setupOnce` and it would
* potentially create a transaction before some native SDK integrations have run
* and applied their own global event processor. An example is:
* https://github.com/getsentry/sentry-javascript/blob/b47ceafbdac7f8b99093ce6023726ad4687edc48/packages/browser/src/integrations/useragent.ts
*
* So we call `replay.setup` in next event loop as a workaround to wait for other
* global event processors to finish. This is no longer needed, but keeping it
* here to avoid any future issues.
*/
setupOnce(): void {
if (!isBrowser()) {
return;
}

this._setup();
this._hasCalledSetupOnce = true;

// XXX: See method comments above
setTimeout(() => this.start());
}

/**
* Initializes the plugin.
*
* Creates or loads a session, attaches listeners to varying events (DOM,
* PerformanceObserver, Recording, Sentry SDK, etc)
*/
start(): void {
if (!this._replay) {
return;
}

this._replay.start();
}

/**
* Currently, this needs to be manually called (e.g. for tests). Sentry SDK
* does not support a teardown
*/
stop(): void {
if (!this._replay) {
return;
}

this._replay.stop();
}

private _setup(): void {
// Client is not available in constructor, so we need to wait until setupOnce
this._loadReplayOptionsFromClient();

this._replay = new ReplayContainer({
options: this.options,
recordingOptions: this.recordingOptions,
});
}

/** Parse Replay-related options from SDK options */
private _loadReplayOptionsFromClient(): void {
const client = getCurrentHub().getClient() as BrowserClient | undefined;
const opt = client && (client.getOptions() as BrowserOptions | undefined);

if (opt && typeof opt.replaysSessionSampleRate === 'number') {
this.options.sessionSampleRate = opt.replaysSessionSampleRate;
}

if (opt && typeof opt.replaysOnErrorSampleRate === 'number') {
this.options.errorSampleRate = opt.replaysOnErrorSampleRate;
}
}
}
export { Replay } from './integration';
195 changes: 195 additions & 0 deletions packages/replay/src/integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import type { BrowserClient, BrowserOptions } from '@sentry/browser';
import { getCurrentHub } from '@sentry/core';
import { Integration } from '@sentry/types';

import { DEFAULT_ERROR_SAMPLE_RATE, DEFAULT_SESSION_SAMPLE_RATE } from './constants';
import { ReplayContainer } from './replay';
import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions } from './types';
import { captureInternalException } from './util/captureInternalException';
import { isBrowser } from './util/isBrowser';

const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed,map,audio';

let _initialized = false;

export class Replay implements Integration {
/**
* @inheritDoc
*/
public static id: string = 'Replay';

/**
* @inheritDoc
*/
public name: string = Replay.id;

/**
* Options to pass to `rrweb.record()`
*/
readonly recordingOptions: RecordingOptions;

readonly options: ReplayPluginOptions;

/** In tests, this is only called the first time */
protected _hasCalledSetupOnce: boolean = false;

private _replay?: ReplayContainer;

constructor({
flushMinDelay = 5000,
flushMaxDelay = 15000,
initialFlushDelay = 5000,
stickySession = true,
useCompression = true,
sessionSampleRate,
errorSampleRate,
maskAllText = true,
maskAllInputs = true,
blockAllMedia = true,
blockClass = 'sentry-block',
ignoreClass = 'sentry-ignore',
maskTextClass = 'sentry-mask',
blockSelector = '[data-sentry-block]',
...recordingOptions
}: ReplayConfiguration = {}) {
this.recordingOptions = {
maskAllInputs,
blockClass,
ignoreClass,
maskTextClass,
blockSelector,
...recordingOptions,
};

this.options = {
flushMinDelay,
flushMaxDelay,
stickySession,
initialFlushDelay,
sessionSampleRate: DEFAULT_SESSION_SAMPLE_RATE,
errorSampleRate: DEFAULT_ERROR_SAMPLE_RATE,
useCompression,
maskAllText,
blockAllMedia,
};

if (typeof sessionSampleRate === 'number') {
// eslint-disable-next-line
console.warn(
`[Replay] You are passing \`sessionSampleRate\` to the Replay integration.
This option is deprecated and will be removed soon.
Instead, configure \`replaysSessionSampleRate\` directly in the SDK init options, e.g.:
Sentry.init({ replaysSessionSampleRate: ${sessionSampleRate} })`,
);

this.options.sessionSampleRate = sessionSampleRate;
}

if (typeof errorSampleRate === 'number') {
// eslint-disable-next-line
console.warn(
`[Replay] You are passing \`errorSampleRate\` to the Replay integration.
This option is deprecated and will be removed soon.
Instead, configure \`replaysOnErrorSampleRate\` directly in the SDK init options, e.g.:
Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
);

this.options.errorSampleRate = errorSampleRate;
}

if (this.options.maskAllText) {
// `maskAllText` is a more user friendly option to configure
// `maskTextSelector`. This means that all nodes will have their text
// content masked.
this.recordingOptions.maskTextSelector = '*';
}

if (this.options.blockAllMedia) {
// `blockAllMedia` is a more user friendly option to configure blocking
// embedded media elements
this.recordingOptions.blockSelector = !this.recordingOptions.blockSelector
? MEDIA_SELECTORS
: `${this.recordingOptions.blockSelector},${MEDIA_SELECTORS}`;
}

if (isBrowser() && _initialized) {
const error = new Error('Multiple Sentry Session Replay instances are not supported');
captureInternalException(error);
throw error;
}

_initialized = true;
}

/**
* We previously used to create a transaction in `setupOnce` and it would
* potentially create a transaction before some native SDK integrations have run
* and applied their own global event processor. An example is:
* https://github.com/getsentry/sentry-javascript/blob/b47ceafbdac7f8b99093ce6023726ad4687edc48/packages/browser/src/integrations/useragent.ts
*
* So we call `replay.setup` in next event loop as a workaround to wait for other
* global event processors to finish. This is no longer needed, but keeping it
* here to avoid any future issues.
*/
setupOnce(): void {
if (!isBrowser()) {
return;
}

this._setup();
this._hasCalledSetupOnce = true;

// XXX: See method comments above
setTimeout(() => this.start());
}

/**
* Initializes the plugin.
*
* Creates or loads a session, attaches listeners to varying events (DOM,
* PerformanceObserver, Recording, Sentry SDK, etc)
*/
start(): void {
if (!this._replay) {
return;
}

this._replay.start();
}

/**
* Currently, this needs to be manually called (e.g. for tests). Sentry SDK
* does not support a teardown
*/
stop(): void {
if (!this._replay) {
return;
}

this._replay.stop();
}

private _setup(): void {
// Client is not available in constructor, so we need to wait until setupOnce
this._loadReplayOptionsFromClient();

this._replay = new ReplayContainer({
options: this.options,
recordingOptions: this.recordingOptions,
});
}

/** Parse Replay-related options from SDK options */
private _loadReplayOptionsFromClient(): void {
const client = getCurrentHub().getClient() as BrowserClient | undefined;
const opt = client && (client.getOptions() as BrowserOptions | undefined);

if (opt && typeof opt.replaysSessionSampleRate === 'number') {
this.options.sessionSampleRate = opt.replaysSessionSampleRate;
}

if (opt && typeof opt.replaysOnErrorSampleRate === 'number') {
this.options.errorSampleRate = opt.replaysOnErrorSampleRate;
}
}
}