diff --git a/package.json b/package.json index 351cec2d8df1..9bbd68de81d3 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "karma-firefox-launcher": "^1.1.0", "lerna": "3.13.4", "madge": "4.0.2", + "magic-string": "^0.27.0", "mocha": "^6.1.4", "nodemon": "^2.0.16", "npm-run-all": "^4.1.5", diff --git a/packages/browser/package.json b/packages/browser/package.json index 7bbbf85ce19f..b0844339361b 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@sentry/core": "7.26.0", + "@sentry/replay": "7.26.0", "@sentry/types": "7.26.0", "@sentry/utils": "7.26.0", "tslib": "^1.9.3" diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 8b88f7fbd6e3..9f5aa0d4e70b 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -1,5 +1,13 @@ import { BaseClient, getEnvelopeEndpointWithUrlEncodedAuth, Scope, SDK_VERSION } from '@sentry/core'; -import { ClientOptions, Event, EventHint, Options, Severity, SeverityLevel } from '@sentry/types'; +import { + BrowserClientReplayOptions, + ClientOptions, + Event, + EventHint, + Options, + Severity, + SeverityLevel, +} from '@sentry/types'; import { createClientReportEnvelope, dsnToString, logger, serializeEnvelope } from '@sentry/utils'; import { eventFromException, eventFromMessage } from './eventbuilder'; @@ -7,22 +15,6 @@ import { WINDOW } from './helpers'; import { Breadcrumbs } from './integrations'; import { BREADCRUMB_INTEGRATION_ID } from './integrations/breadcrumbs'; import { BrowserTransportOptions } from './transports/types'; - -type BrowserClientReplayOptions = { - /** - * The sample rate for session-long replays. - * 1.0 will record all sessions and 0 will record none. - */ - replaysSessionSampleRate?: number; - - /** - * The sample rate for sessions that has had an error occur. - * This is independent of `sessionSampleRate`. - * 1.0 will record all sessions and 0 will record none. - */ - replaysOnErrorSampleRate?: number; -}; - /** * Configuration options for the Sentry Browser SDK. * @see @sentry/types Options for more information. diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index f98d926b4f5c..8185cbc6259e 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -19,3 +19,12 @@ const INTEGRATIONS = { }; export { INTEGRATIONS as Integrations }; + +// DO NOT DELETE THESE COMMENTS! +// We want to exclude Replay from CDN bundles, so we remove the block below with our +// excludeReplay Rollup plugin when generating bundles. Everything between +// ROLLUP_EXCLUDE_FROM_BUNDLES_BEGIN and _END__ is removed for bundles. + +// __ROLLUP_EXCLUDE_FROM_BUNDLES_BEGIN__ +export { Replay } from '@sentry/replay'; +// __ROLLUP_EXCLUDE_FROM_BUNDLES_END__ diff --git a/packages/replay/package.json b/packages/replay/package.json index 3ad219406d76..b57188f6e4fc 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -46,7 +46,6 @@ "homepage": "https://github.com/getsentry/sentry-replay#readme", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry/browser": "7.26.0", "@types/lodash.debounce": "4.0.7", "@types/pako": "^2.0.0", "jsdom-worker": "^0.2.1", diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 97ef5a2eb903..c30c4051f2d5 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -1,5 +1,5 @@ -import type { BrowserClient, BrowserOptions } from '@sentry/browser'; import { getCurrentHub } from '@sentry/core'; +import type { BrowserClientReplayOptions } from '@sentry/types'; import { Integration } from '@sentry/types'; import { DEFAULT_ERROR_SAMPLE_RATE, DEFAULT_SESSION_SAMPLE_RATE } from './constants'; @@ -184,8 +184,8 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, /** 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); + const client = getCurrentHub().getClient(); + const opt = client && (client.getOptions() as BrowserClientReplayOptions | undefined); if (opt && typeof opt.replaysSessionSampleRate === 'number') { this.options.sessionSampleRate = opt.replaysSessionSampleRate; diff --git a/packages/types/src/browseroptions.ts b/packages/types/src/browseroptions.ts new file mode 100644 index 000000000000..9eeea6350728 --- /dev/null +++ b/packages/types/src/browseroptions.ts @@ -0,0 +1,18 @@ +/** + * Options added to the Browser SDK's init options that are specific for Replay. + * Note: This type was moved to @sentry/types to avoid a circular dependency between Browser and Replay. + */ +export type BrowserClientReplayOptions = { + /** + * The sample rate for session-long replays. + * 1.0 will record all sessions and 0 will record none. + */ + replaysSessionSampleRate?: number; + + /** + * The sample rate for sessions that has had an error occur. + * This is independent of `sessionSampleRate`. + * 1.0 will record all sessions and 0 will record none. + */ + replaysOnErrorSampleRate?: number; +}; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f3bc63feda4c..e6e477b2efc0 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -93,3 +93,5 @@ export type { export type { User, UserFeedback } from './user'; export type { WrappedFunction } from './wrappedfunction'; export type { Instrumenter } from './instrumenter'; + +export type { BrowserClientReplayOptions } from './browseroptions'; diff --git a/rollup/bundleHelpers.js b/rollup/bundleHelpers.js index 41888d54f77e..e5870d7aaba8 100644 --- a/rollup/bundleHelpers.js +++ b/rollup/bundleHelpers.js @@ -16,6 +16,7 @@ import { makeSucrasePlugin, makeTerserPlugin, makeTSPlugin, + makeExcludeReplayPlugin, } from './plugins/index.js'; import { mergePlugins } from './utils'; @@ -30,6 +31,7 @@ export function makeBaseBundleConfig(options) { const markAsBrowserBuildPlugin = makeBrowserBuildPlugin(true); const licensePlugin = makeLicensePlugin(licenseTitle); const tsPlugin = makeTSPlugin(jsVersion.toLowerCase()); + const excludeReplayPlugin = makeExcludeReplayPlugin(); // The `commonjs` plugin is the `esModuleInterop` of the bundling world. When used with `transformMixedEsModules`, it // will include all dependencies, imported or required, in the final bundle. (Without it, CJS modules aren't included @@ -43,7 +45,7 @@ export function makeBaseBundleConfig(options) { name: 'Sentry', }, context: 'window', - plugins: [markAsBrowserBuildPlugin], + plugins: [markAsBrowserBuildPlugin, excludeReplayPlugin], }; // used by `@sentry/integrations` and `@sentry/wasm` (bundles which need to be combined with a stand-alone SDK bundle) diff --git a/rollup/plugins/bundlePlugins.js b/rollup/plugins/bundlePlugins.js index d8f85ab1b1d8..78396a6025e7 100644 --- a/rollup/plugins/bundlePlugins.js +++ b/rollup/plugins/bundlePlugins.js @@ -8,6 +8,8 @@ * Typescript plugin docs: https://github.com/ezolenko/rollup-plugin-typescript2 */ +import path from 'path'; + import commonjs from '@rollup/plugin-commonjs'; import deepMerge from 'deepmerge'; import license from 'rollup-plugin-license'; @@ -15,6 +17,7 @@ import resolve from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; import { terser } from 'rollup-plugin-terser'; import typescript from 'rollup-plugin-typescript2'; +import MagicString from 'magic-string'; /** * Create a plugin to add an identification banner to the top of stand-alone bundles. @@ -164,6 +167,37 @@ export function makeTSPlugin(jsVersion) { return plugin; } +/** + * Creates a Rollup plugin that removes all code between the `__ROLLUP_EXCLUDE_FROM_BUNDLES_BEGIN__` + * and `__ROLLUP_EXCLUDE_FROM_BUNDLES_END__` comment guards. This is used to exclude the Replay integration + * from the browser and browser+tracing bundles. + * If we need to add more such guards in the future, we might want to refactor this into a more generic plugin. + */ +export function makeExcludeReplayPlugin() { + const replacementRegex = /\/\/ __ROLLUP_EXCLUDE_FROM_BUNDLES_BEGIN__(.|\n)*__ROLLUP_EXCLUDE_FROM_BUNDLES_END__/m; + const browserIndexFilePath = path.resolve(__dirname, '../../packages/browser/src/index.ts'); + + const plugin = { + transform(code, id) { + const isBrowserIndexFile = path.resolve(id) === browserIndexFilePath; + if (!isBrowserIndexFile || !replacementRegex.test(code)) { + return null; + } + + const ms = new MagicString(code); + const transformedCode = ms.replace(replacementRegex, ''); + return { + code: transformedCode.toString(), + map: transformedCode.generateMap({ hires: true }), + }; + }, + }; + + plugin.name = 'excludeReplay'; + + return plugin; +} + // We don't pass these plugins any options which need to be calculated or changed by us, so no need to wrap them in // another factory function, as they are themselves already factory functions. export { resolve as makeNodeResolvePlugin }; diff --git a/rollup/utils.js b/rollup/utils.js index 6a7462788a47..81fed5949f97 100644 --- a/rollup/utils.js +++ b/rollup/utils.js @@ -16,10 +16,12 @@ export const insertAt = (arr, index, ...insertees) => { export function mergePlugins(pluginsA, pluginsB) { const plugins = [...pluginsA, ...pluginsB]; plugins.sort((a, b) => { - // Hacky way to make sure the ones we care about end up where they belong in the order. (Really the TS and sucrase + // Hacky way to make sure the ones we care about end up where they belong in the order. Really the TS and sucrase // plugins are tied - both should come first - but they're mutually exclusive, so they can come in arbitrary order - // here.) - const order = ['typescript', 'sucrase', '...', 'terser', 'license']; + // here. + // Additionally, the excludeReplay plugin must run before TS/Sucrase so that we can eliminate the replay code + // before anything is type-checked (TS-only) and transpiled. + const order = ['excludeReplay', 'typescript', 'sucrase', '...', 'terser', 'license']; const sortKeyA = order.includes(a.name) ? a.name : '...'; const sortKeyB = order.includes(b.name) ? b.name : '...'; diff --git a/yarn.lock b/yarn.lock index 61035c481cda..46ba255b5531 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2232,7 +2232,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== @@ -16502,6 +16502,13 @@ magic-string@^0.26.2: dependencies: sourcemap-codec "^1.4.8" +magic-string@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" + integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.13" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"