From c1848cd8c856471b0f65e349f4822fc5a0ff59c8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 2 Feb 2024 08:43:40 +0100 Subject: [PATCH] feat(bundles): Add pluggable integrations on CDN to `Sentry` namespace (#10452) Previously, they were only put on `Sentry.Integrations.XXX`, now you can do e.g. `Sentry.httpClientIntegration()`. While at it, I also added a browser integration test for this. I also made the way we do this more future proof, as in v8 this will not be imported anymore from `@sentry/integrations` (which we rely on right now), plus the heuristic used to rely on integration name === filename. Now, there is a manual map of imported method names to a CDN bundle file name. --- .../httpclient/httpClientIntegration/init.js | 11 ++++ .../httpClientIntegration/subject.js | 8 +++ .../httpclient/httpClientIntegration/test.ts | 64 +++++++++++++++++++ .../utils/generatePlugin.ts | 32 ++++++++-- dev-packages/rollup-utils/bundleHelpers.mjs | 1 + 5 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js new file mode 100644 index 000000000000..07bc4a5b351e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; +import { httpClientIntegration } from '@sentry/integrations'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [httpClientIntegration()], + tracesSampleRate: 1, + sendDefaultPii: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js new file mode 100644 index 000000000000..7a2e3cdd28c0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js @@ -0,0 +1,8 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('GET', 'http://localhost:7654/foo', true); +xhr.withCredentials = true; +xhr.setRequestHeader('Accept', 'application/json'); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.setRequestHeader('Cache', 'no-cache'); +xhr.send(); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts new file mode 100644 index 000000000000..8bf8efa34cc4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts @@ -0,0 +1,64 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('works with httpClientIntegration', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 500, + body: JSON.stringify({ + error: { + message: 'Internal Server Error', + }, + }), + headers: { + 'Content-Type': 'text/html', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + + // Not able to get the cookies from the request/response because of Playwright bug + // https://github.com/microsoft/playwright/issues/11035 + expect(eventData).toMatchObject({ + message: 'HTTP Client Error with status code: 500', + exception: { + values: [ + { + type: 'Error', + value: 'HTTP Client Error with status code: 500', + mechanism: { + type: 'http.client', + handled: false, + }, + }, + ], + }, + request: { + url: 'http://localhost:7654/foo', + method: 'GET', + headers: { + accept: 'application/json', + cache: 'no-cache', + 'content-type': 'application/json', + }, + }, + contexts: { + response: { + status_code: 500, + body_size: 45, + headers: { + 'content-type': 'text/html', + 'content-length': '45', + }, + }, + }, + }); +}); diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index 1258c684492d..cf2816ab0033 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -22,6 +22,27 @@ const useCompiledModule = bundleKey === 'esm' || bundleKey === 'cjs'; const useBundleOrLoader = bundleKey && !useCompiledModule; const useLoader = bundleKey.startsWith('loader'); +// These are imports that, when using CDN bundles, are not included in the main CDN bundle. +// In this case, if we encounter this import, we want to add this CDN bundle file instead +const IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS: Record = { + httpClientIntegration: 'httpclient', + HttpClient: 'httpclient', + captureConsoleIntegration: 'captureconsole', + CaptureConsole: 'captureconsole', + debugIntegration: 'debug', + Debug: 'debug', + rewriteFramesIntegration: 'rewriteframes', + RewriteFrames: 'rewriteframes', + contextLinesIntegration: 'contextlines', + ContextLines: 'contextlines', + extraErrorDataIntegration: 'extraerrordata', + ExtraErrorData: 'extraerrordata', + reportingObserverIntegration: 'reportingobserver', + ReportingObserver: 'reportingobserver', + sessionTimingIntegration: 'sessiontiming', + SessionTiming: 'sessiontiming', +}; + const BUNDLE_PATHS: Record> = { browser: { cjs: 'build/npm/cjs/index.js', @@ -149,8 +170,8 @@ class SentryScenarioGenerationPlugin { '@sentry/browser': 'Sentry', '@sentry/tracing': 'Sentry', '@sentry/replay': 'Sentry', - '@sentry/integrations': 'Sentry.Integrations', - '@sentry/wasm': 'Sentry.Integrations', + '@sentry/integrations': 'Sentry', + '@sentry/wasm': 'Sentry', } : {}; @@ -161,8 +182,11 @@ class SentryScenarioGenerationPlugin { parser.hooks.import.tap( this._name, (statement: { specifiers: [{ imported: { name: string } }] }, source: string) => { - if (source === '@sentry/integrations') { - this.requiredIntegrations.push(statement.specifiers[0].imported.name.toLowerCase()); + const imported = statement.specifiers?.[0]?.imported?.name; + + if (imported && IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS[imported]) { + const bundleName = IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS[imported]; + this.requiredIntegrations.push(bundleName); } else if (source === '@sentry/wasm') { this.requiresWASMIntegration = true; } diff --git a/dev-packages/rollup-utils/bundleHelpers.mjs b/dev-packages/rollup-utils/bundleHelpers.mjs index b6ca7c8fcbc7..66bded3b62de 100644 --- a/dev-packages/rollup-utils/bundleHelpers.mjs +++ b/dev-packages/rollup-utils/bundleHelpers.mjs @@ -82,6 +82,7 @@ export function makeBaseBundleConfig(options) { ' for (var key in exports) {', ' if (Object.prototype.hasOwnProperty.call(exports, key)) {', ' __window.Sentry.Integrations[key] = exports[key];', + ' __window.Sentry[key] = exports[key];', ' }', ' }', ].join('\n'),