From 3340443e808805821170addf16c3bdf9e40853c4 Mon Sep 17 00:00:00 2001 From: Tim Hostetler <6970899+thostetler@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:25:28 -0400 Subject: [PATCH 1/4] feat: modernize Sentry integration with latest SDK v10.21.0 - Upgrade @sentry/cli from v2.38.0 to v2.57.0 - Replace custom Sentry loader with official CDN bundle approach - Switch to browser.sentry-cdn.com with proper integrity hash - Simplify initialization by removing complex queue management - Maintain backward compatibility with existing window.whenSentryReady/getSentry APIs - Keep all existing configuration (tracing, replay, environment settings) - Reduce complexity while improving security and maintainability The modernized setup follows current Sentry best practices and will be easier to maintain going forward while preserving all existing functionality. --- package-lock.json | 140 ++++++++++++++++++++++++++++++++++++++----- src/index.html | 148 ++++++++++++++++++++++++---------------------- 2 files changed, 202 insertions(+), 86 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5cdd8799b..b5dc7c89d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1578,9 +1578,9 @@ } }, "node_modules/@sentry/cli": { - "version": "2.38.0", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.38.0.tgz", - "integrity": "sha512-ld9+1GdPkDaFr6T4SGocxoMcrBB/K6Z37TvBx8IMrDQC+eJDkBFiyqmHnzrj/8xoj5O220pqjPZCfvqzH268sQ==", + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.57.0.tgz", + "integrity": "sha512-oC4HPrVIX06GvUTgK0i+WbNgIA9Zl5YEcwf9N4eWFJJmjonr2j4SML9Hn2yNENbUWDgwepy4MLod3P8rM4bk/w==", "hasInstallScript": true, "dependencies": { "https-proxy-agent": "^5.0.0", @@ -1596,26 +1596,138 @@ "node": ">= 10" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.38.0", - "@sentry/cli-linux-arm": "2.38.0", - "@sentry/cli-linux-arm64": "2.38.0", - "@sentry/cli-linux-i686": "2.38.0", - "@sentry/cli-linux-x64": "2.38.0", - "@sentry/cli-win32-i686": "2.38.0", - "@sentry/cli-win32-x64": "2.38.0" + "@sentry/cli-darwin": "2.57.0", + "@sentry/cli-linux-arm": "2.57.0", + "@sentry/cli-linux-arm64": "2.57.0", + "@sentry/cli-linux-i686": "2.57.0", + "@sentry/cli-linux-x64": "2.57.0", + "@sentry/cli-win32-arm64": "2.57.0", + "@sentry/cli-win32-i686": "2.57.0", + "@sentry/cli-win32-x64": "2.57.0" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.57.0.tgz", + "integrity": "sha512-v1wYQU3BcCO+Z3OVxxO+EnaW4oQhuOza6CXeYZ0z5ftza9r0QQBLz3bcZKTVta86xraNm0z8GDlREwinyddOxQ==", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.57.0.tgz", + "integrity": "sha512-uNHB8xyygqfMd1/6tFzl9NUkuVefg7jdZtM/vVCQVaF/rJLWZ++Wms+LLhYyKXKN8yd7J9wy7kTEl4Qu4jWbGQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.57.0.tgz", + "integrity": "sha512-Kh1jTsMV5Fy/RvB381N/woXe1qclRMqsG6kM3Gq6m6afEF/+k3PyQdNW3HXAola6d63EptokLtxPG2xjWQ+w9Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.57.0.tgz", + "integrity": "sha512-EYXghoK/tKd0zqz+KD/ewXXE3u1HLCwG89krweveytBy/qw7M5z58eFvw+iGb1Vnbl1f/fRD0G4E0AbEsPfmpg==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" } }, "node_modules/@sentry/cli-linux-x64": { - "version": "2.38.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.38.0.tgz", - "integrity": "sha512-yY593xXbf2W+afyHKDvO4QJwoWQX97/K0NYUAqnpg3TVmIfLV9DNVid+M1w6vKIif6n8UQgAFWtR1Ys4P75mBg==", + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.57.0.tgz", + "integrity": "sha512-CyZrP/ssHmAPLSzfd4ydy7icDnwmDD6o3QjhkWwVFmCd+9slSBMQxpIqpamZmrWE6X4R+xBRbSUjmdoJoZ5yMw==", "cpu": [ "x64" ], "optional": true, "os": [ "linux", - "freebsd" + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-arm64": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.57.0.tgz", + "integrity": "sha512-wji/GGE4Lh5I/dNCsuVbg6fRvttvZRG6db1yPW1BSvQRh8DdnVy1CVp+HMqSq0SRy/S4z60j2u+m4yXMoCL+5g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.57.0.tgz", + "integrity": "sha512-hWvzyD7bTPh3b55qvJ1Okg3Wbl0Km8xcL6KvS7gfBl6uss+I6RldmQTP0gJKdHSdf/QlJN1FK0b7bLnCB3wHsg==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.57.0.tgz", + "integrity": "sha512-QWYV/Y0sbpDSTyA4XQBOTaid4a6H2Iwa1Z8UI+qNxFlk0ADSEgIqo2NrRHDU8iRnghTkecQNX1NTt/7mXN3f/A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" ], "engines": { "node": ">=10" diff --git a/src/index.html b/src/index.html index e7c696446..119193cdc 100644 --- a/src/index.html +++ b/src/index.html @@ -250,92 +250,96 @@

// Simulate a process.env-like environment window.process = { env: { NODE_ENV: window.ENV ?? 'production' } }; - // Centralized Sentry loader state and helpers - (function initSentryLoader() { - const state = (window.__sentryState ||= { ready: false, error: null, queue: [] }); - - function flushQueue() { - const q = state.queue.splice(0, state.queue.length); - q.forEach(({ cb, errCb, res, rej }) => { - if (state.ready && window.Sentry) { - try { if (cb) cb.call(window, window.Sentry); if (res) res(window.Sentry); } - catch (e) { if (errCb) errCb.call(window, e); if (rej) rej(e); } - } else { - const err = state.error || new Error('Sentry failed to load'); - if (errCb) errCb.call(window, err); - if (rej) rej(err); - } - }); + // Modern helper for accessing Sentry + const withSentry = function (callback) { + if (typeof window.Sentry !== 'undefined') { + try { + callback(window.Sentry); + } catch (e) { + // Silently handle errors in callback + } } + }; - // Back-compat callback API; queues until Sentry is ready - window.getSentry = function(cb, errCb = () => {}) { - if (state.ready && window.Sentry) { - try { cb.call(window, window.Sentry); } catch (e) { errCb.call(window, e); } - return; + // Legacy compatibility helpers for smooth transition + window.whenSentryReady = function() { + return new Promise((resolve) => { + if (typeof window.Sentry !== 'undefined') { + resolve(window.Sentry); + } else { + // If Sentry isn't loaded yet, wait for it + const checkSentry = () => { + if (typeof window.Sentry !== 'undefined') { + resolve(window.Sentry); + } else { + setTimeout(checkSentry, 50); + } + }; + checkSentry(); } - if (state.error) { errCb.call(window, state.error); return; } - state.queue.push({ cb, errCb }); - }; - - // Promise-based helper for newer code paths - window.whenSentryReady = function() { - return new Promise((res, rej) => { - if (state.ready && window.Sentry) { res(window.Sentry); return; } - if (state.error) { rej(state.error); return; } - state.queue.push({ res, rej }); - }); - }; + }); + }; - // Called after the SDK finishes loading - window.sentryOnLoad = function() { + window.getSentry = function(callback, errorCallback = () => {}) { + if (typeof window.Sentry !== 'undefined') { try { - if (state.ready && window.Sentry) { return; } - Sentry.init({ - dsn: 'https://46062cbe0aeb7a3b2bb4c3a9b8cd1ac7@o1060269.ingest.us.sentry.io/4507341192036352', - tracesSampleRate: window.ENV === 'development' ? 1.0 : 0.75, - debug: false, - allowUrls: [ - /https:\/\/.*\.adsabs\.harvard\.edu\/.*/, - /https:\/\/code\.jquery\.com\/.*/, - /https:\/\/cdn\.jsdelivr\.net\/.*/, - ], - replaysSessionSampleRate: 0, - replaysOnErrorSampleRate: 0.01, - environment: window.ENV ?? 'production', - integrations: [ - Sentry.browserTracingIntegration(), - Sentry.replayIntegration({ - maskAllText: false, - blockAllMedia: false, - unmask: ['[name=q]'], - }), - ], - beforeSendSpan: tagSpans - }); - state.ready = true; - flushQueue(); + callback(window.Sentry); } catch (e) { - state.error = e; - flushQueue(); + errorCallback(e); } - }; + } else { + // If Sentry isn't loaded yet, wait for it + const checkSentry = () => { + if (typeof window.Sentry !== 'undefined') { + try { + callback(window.Sentry); + } catch (e) { + errorCallback(e); + } + } else { + setTimeout(checkSentry, 50); + } + }; + checkSentry(); + } + }; - // Called if the SDK fails to load - window.sentryOnError = function(e) { - state.error = e instanceof Error ? e : new Error('Sentry script failed to load'); - flushQueue(); - }; - })(); + // Initialize Sentry after the script loads + function initSentry() { + if (typeof Sentry !== 'undefined') { + Sentry.init({ + dsn: 'https://46062cbe0aeb7a3b2bb4c3a9b8cd1ac7@o1060269.ingest.us.sentry.io/4507341192036352', + tracesSampleRate: window.ENV === 'development' ? 1.0 : 0.75, + debug: false, + allowUrls: [ + /https:\/\/.*\.adsabs\.harvard\.edu\/.*/, + /https:\/\/code\.jquery\.com\/.*/, + /https:\/\/cdn\.jsdelivr\.net\/.*/, + ], + replaysSessionSampleRate: 0, + replaysOnErrorSampleRate: 0.01, + environment: window.ENV ?? 'production', + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration({ + maskAllText: false, + blockAllMedia: false, + unmask: ['[name=q]'], + }), + ], + beforeSendSpan: tagSpans + }); + } + } From 9dd8ea48c9fa52fd221cda15f32d1d4af0c3e347 Mon Sep 17 00:00:00 2001 From: Tim Hostetler <6970899+thostetler@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:39:37 -0400 Subject: [PATCH 2/4] Optimize Sentry replay configuration based on project error analysis - Add targeted filtering for high-volume error patterns identified in Sentry data - Filter RequireJS script loading errors (BUMBLEBEE-D: 13k events) - Filter Google Tag Manager __tcfapi errors (BUMBLEBEE-7QW/7QV: 6.8k events) - Filter syntax errors from invalid tokens (BUMBLEBEE-1TC: 172k events) - Filter jQuery/Bootstrap loading issues (~5k events combined) - Filter third-party script errors (GA, Pinterest, CDN) - Implement daily replay quotas (100/day prod, 1000/day dev) - Add aggressive payload size reduction (block media, limit network capture) - Enable logging and Core Web Vitals experimental spans - Expected ~99.9% reduction in replay usage while maintaining debug value Fixes: BUMBLEBEE-1TC, BUMBLEBEE-D, BUMBLEBEE-7QW, BUMBLEBEE-7QV, BUMBLEBEE-1R2 --- SENTRY_REPLAY_OPTIMIZATION.md | 96 +++++++++++++ src/index.html | 244 +++++++++++++++++++++++++++++++++- 2 files changed, 335 insertions(+), 5 deletions(-) create mode 100644 SENTRY_REPLAY_OPTIMIZATION.md diff --git a/SENTRY_REPLAY_OPTIMIZATION.md b/SENTRY_REPLAY_OPTIMIZATION.md new file mode 100644 index 000000000..5f7e97f37 --- /dev/null +++ b/SENTRY_REPLAY_OPTIMIZATION.md @@ -0,0 +1,96 @@ +# Sentry Replay Optimization Summary + +## Problem Identified +Your project was burning through 100k replay allowance in 8-10 hours due to high-volume, low-value errors triggering replays unnecessarily. + +## Key Issues Consuming Replay Budget + +Based on analysis of your actual Sentry data, these error patterns were consuming most replays: + +1. **BUMBLEBEE-1TC** - 172,785 events - `SyntaxError: Invalid or unexpected token` +2. **BUMBLEBEE-D** - 13,235 events - RequireJS script loading errors +3. **BUMBLEBEE-7QW/7QV** - 6,787 events - Google Tag Manager `__tcfapi` errors +4. **BUMBLEBEE-1R2** - 7,459 events - Uncompressed asset performance issues +5. **BUMBLEBEE-75Y** - 3,271 events - Search cycle failures (HTTP 429 rate limiting) +6. **BUMBLEBEE-5KN** - 2,584 events - Google Analytics loading issues +7. **Multiple jQuery/Bootstrap issues** - ~5,000 events combined + +## Optimizations Implemented + +### 1. Smart Error Filtering +- **High-volume error patterns**: Added specific regex patterns for your top error types +- **Third-party script filtering**: Filters errors from GTM, GA, Pinterest, etc. +- **RequireJS/Script loading errors**: Filters common module loading failures +- **Network/fetch failures**: Filters transient network issues +- **Performance issues**: Filters asset compression warnings + +### 2. Aggressive Replay Sampling +- **No random session recording**: `replaysSessionSampleRate: 0` +- **Daily quota limits**: 100 replays/day in production, 1000 in development +- **Ultra-low sampling rates**: 5% for critical app errors, 0.1% for others +- **Smart quota management**: Uses localStorage to track daily usage + +### 3. Payload Size Reduction +- **Block all media**: Images/videos not recorded +- **Minimal network capture**: Only critical API endpoints +- **No request/response bodies**: Reduces network payload size +- **Limited DOM mutations**: Caps mutation tracking at 10,000 events +- **Canvas/font optimization**: Disabled canvas recording and font collection +- **5-minute replay limit**: Prevents excessively long recordings + +### 4. Targeted Network Monitoring +Only captures network requests for: +- `/api/v1/` - Core API calls +- `/search/query` - Search queries +- `/resolver/` - Citation resolver +- `/export/` - Export functionality +- `/user/` - User management +- `/myads/` - User libraries + +## Expected Impact +- **~99.9% reduction in replay usage** +- **Elimination of noise from third-party scripts** +- **Focus on genuine application errors** +- **Smaller replay file sizes** +- **Better signal-to-noise ratio for debugging** + +## Optional: Additional CSS Optimizations + +Add these CSS classes to further reduce replay payload: + +```css +/* Block these elements from Sentry replays */ +.sentry-block { + /* Applied to: ads, tracking pixels, large images, videos */ +} + +/* Ignore these elements completely in replays */ +.sentry-ignore { + /* Applied to: analytics widgets, social media embeds */ +} +``` + +Add classes to HTML elements like: +```html + + + + +
+``` + +## Monitoring + +1. **Check daily usage**: Monitor localStorage key `sentry_replay_quota` +2. **Review filtered errors**: Check if important errors are being filtered +3. **Adjust sampling rates**: Fine-tune based on actual usage patterns +4. **Monitor payload sizes**: Ensure replays stay under size limits + +## Next Steps + +1. Deploy these changes and monitor replay usage over 24-48 hours +2. Review which errors are still generating replays in Sentry dashboard +3. Adjust filtering patterns if needed based on new data +4. Consider adding CSS classes to further optimize payload sizes + +This should bring your replay usage down to a sustainable level while maintaining visibility into genuine application issues. \ No newline at end of file diff --git a/src/index.html b/src/index.html index 119193cdc..92a2b09ee 100644 --- a/src/index.html +++ b/src/index.html @@ -250,6 +250,22 @@

// Simulate a process.env-like environment window.process = { env: { NODE_ENV: window.ENV ?? 'production' } }; + /* + * SENTRY REPLAY OPTIMIZATION FOR HIGH-TRAFFIC SITES + * + * This configuration is designed to drastically reduce replay usage while + * still capturing valuable debugging information: + * + * 1. NO random session recording (replaysSessionSampleRate: 0) + * 2. Smart error-based sampling with daily quotas + * 3. Filtered out common/expected errors that don't need replays + * 4. Higher priority for critical JavaScript errors + * 5. Reduced payload sizes by blocking media and limiting network capture + * 6. Daily quota limits (100 replays/day in prod, 1000 in dev) + * + * Expected impact: ~99.9% reduction in replay usage + */ + // Modern helper for accessing Sentry const withSentry = function (callback) { if (typeof window.Sentry !== 'undefined') { @@ -304,6 +320,172 @@

} }; + // Daily quota management for replays + function checkDailyReplayQuota() { + const today = new Date().toDateString(); + const quotaKey = 'sentry_replay_quota'; + const dateKey = 'sentry_replay_date'; + + try { + const savedDate = localStorage.getItem(dateKey); + let replayCount = parseInt(localStorage.getItem(quotaKey) || '0'); + + // Reset quota if it's a new day + if (savedDate !== today) { + replayCount = 0; + localStorage.setItem(dateKey, today); + } + + // Daily limit: 100 replays max (adjust as needed) + const DAILY_LIMIT = window.ENV === 'development' ? 1000 : 100; + + if (replayCount >= DAILY_LIMIT) { + return false; + } + + // Increment counter + localStorage.setItem(quotaKey, (replayCount + 1).toString()); + return true; + + } catch (e) { + // If localStorage fails, still allow some replays but be more restrictive + return Math.random() < 0.0001; // 0.01% + } + } + + // Smart replay sampling based on actual project error patterns + function shouldCaptureReplay(errorEvent) { + // Always capture in development (but still respect quota) + if (window.ENV === 'development') { + return checkDailyReplayQuota(); + } + + // Check daily quota first + if (!checkDailyReplayQuota()) { + return false; + } + + const errorMessage = errorEvent?.message || errorEvent?.exception?.values?.[0]?.value || ''; + const errorType = errorEvent?.exception?.values?.[0]?.type || ''; + const stacktrace = errorEvent?.exception?.values?.[0]?.stacktrace?.frames || []; + const filename = stacktrace[0]?.filename || ''; + + // HIGH-VOLUME ERROR PATTERNS FROM PROJECT DATA (BUMBLEBEE issues) + // These patterns are consuming most of your replay quota + const highVolumeFilters = [ + // BUMBLEBEE-1TC: 172k events - Syntax errors from invalid tokens + /Invalid or unexpected token/i, + /SyntaxError.*Invalid.*token/i, + /Unexpected token/i, + + // BUMBLEBEE-D: 13k events - RequireJS script loading + /Script error for.*js\//i, + /RequireJS.*scripterror/i, + /Mismatched anonymous define/i, + /needed by: router/i, + + // BUMBLEBEE-7QW/7QV: 6.8k events - Google Tag Manager + /window\.__tcfapi is not a function/i, + /gtag.*not.*function/i, + /ga.*not.*function/i, + /_gaCustomDimensionUserType.*not defined/i, + + // BUMBLEBEE-5KN: 2.6k events - Google Analytics loading + /window\.ga.*appear/i, + /gtag.*does not appear to load/i, + /Maximum.*retries.*window\.ga/i, + + // Multiple jQuery/Bootstrap issues: ~5k events combined + /jQuery.*not defined/i, + /\$.*not defined/i, + /Bootstrap.*requires jQuery/i, + + // Multiple "Failed to fetch" issues: ~2k events combined + /Failed to fetch/i, + /TypeError.*fetch/i, + /Network request failed/i, + + // BUMBLEBEE-45X: 3k events - Empty values + /Empty values not allowed/i, + + // BUMBLEBEE-75Y: 3.3k events - Search failures (rate limiting) + /Search cycle failed to start/i, + + // Multiple "Cannot read properties" errors: ~2k events + /Cannot read properties of undefined/i, + /Cannot read properties of null/i, + + // Third-party script errors + /pintrk.*not defined/i, + /vbpx.*not defined/i, + /efDataLayer.*not defined/i, + /webVitals.*not defined/i, + + // Performance/Asset issues (BUMBLEBEE-1R2: 7.5k events) + /Uncompressed Asset/i, + /Large.*payload/i, + /Large Render Blocking Asset/i, + + // Common framework noise + /ResizeObserver loop limit exceeded/i, + /Non-Error promise rejection/i, + /ChunkLoadError/i, + /Loading chunk.*failed/i, + /Script error/i, + /Maximum call stack size exceeded/i, + + // Cross-origin and security errors + /Blocked.*from.*cross-origin/i, + /SecurityError.*cross-origin/i, + /Blocked 'connect'/i, + /Blocked 'script'/i, + /Blocked 'font'/i + ]; + + // Third-party domains that generate noise + const thirdPartyFilenames = [ + /googletagmanager\.com/i, + /google-analytics\.com/i, + /gtm\.js/i, + /analytics\.js/i, + /pinterest\.com/i, + /cubox\.pro/i, + /sentry\.io/i, + /cloudflare\.com/i, + /cdn\./i + ]; + + // Filter out high-volume, low-value errors + const isHighVolumeError = highVolumeFilters.some(pattern => + pattern.test(errorMessage) || pattern.test(errorType) + ); + + const isThirdPartyError = thirdPartyFilenames.some(pattern => + pattern.test(filename) + ); + + // Don't capture replays for filtered errors + if (isHighVolumeError || isThirdPartyError) { + return false; + } + + // For remaining errors, sample at very low rates + // These should be genuinely critical application errors + const criticalErrors = [ + /ViewDestroyedError/i, // Framework errors + /getBeeHive.*getService.*not a function/i, // App-specific errors + /Services\.get.*not a function/i // App-specific errors + ]; + + const isAppCritical = criticalErrors.some(pattern => + pattern.test(errorMessage) || pattern.test(errorType) + ); + + // Very conservative sampling even for critical errors + const sampleRate = isAppCritical ? 0.05 : 0.001; // 5% vs 0.1% + return Math.random() < sampleRate; + } + // Initialize Sentry after the script loads function initSentry() { if (typeof Sentry !== 'undefined') { @@ -311,22 +493,74 @@

dsn: 'https://46062cbe0aeb7a3b2bb4c3a9b8cd1ac7@o1060269.ingest.us.sentry.io/4507341192036352', tracesSampleRate: window.ENV === 'development' ? 1.0 : 0.75, debug: false, + enableLogs: true, + _experiments: { + enableStandaloneLcpSpans: true, + enableStandaloneClsSpans: true, + }, allowUrls: [ /https:\/\/.*\.adsabs\.harvard\.edu\/.*/, /https:\/\/code\.jquery\.com\/.*/, /https:\/\/cdn\.jsdelivr\.net\/.*/, ], - replaysSessionSampleRate: 0, - replaysOnErrorSampleRate: 0.01, + // Aggressive replay reduction for high-traffic sites + replaysSessionSampleRate: 0, // No random session recording + replaysOnErrorSampleRate: 0, // Disabled - we'll use beforeSend logic instead environment: window.ENV ?? 'production', integrations: [ Sentry.browserTracingIntegration(), Sentry.replayIntegration({ - maskAllText: false, - blockAllMedia: false, - unmask: ['[name=q]'], + // Privacy and size optimizations + maskAllText: false, // Keep text visible for debugging + blockAllMedia: true, // Block images/videos to reduce payload + blockClass: 'sentry-block', // CSS class to block elements + ignoreClass: 'sentry-ignore', // CSS class to ignore elements + unmask: ['[name=q]', '.search-field', '.query-input'], // Keep search inputs visible + + // Aggressive network filtering - only capture critical API calls + networkDetailAllowUrls: [ + /\/api\/v1\//, // Core API calls + /\/search\/query/, // Search queries + /\/resolver\//, // Citation resolver + /\/export\//, // Export functionality + /\/user\//, // User management + /\/myads\// // User libraries + ], + + // Minimize network data capture + networkCaptureBodies: false, // No request/response bodies + networkRequestHeaders: [], // No request headers + networkResponseHeaders: [], // No response headers + + // Performance optimizations + maxReplayDuration: 300000, // 5 minutes max replay length + sessionSampleRate: 0, // No automatic sessions + errorSampleRate: 0, // We handle this in beforeSend + + // Additional payload reduction + recordCanvas: false, // Don't record canvas elements + collectFonts: false, // Don't capture font loads + inlineStylesheet: false, // Don't inline stylesheets + inlineImages: false, // Don't inline images + + // Only capture minimal DOM mutations + mutationBreadcrumbLimit: 750, // Reduce mutation tracking + mutationLimit: 10000, // Limit DOM mutations captured }), ], + beforeSend(event, hint) { + // Smart replay capture logic + if (hint.originalException && shouldCaptureReplay(event)) { + // Only start replay for this specific error + if (window.Sentry && typeof window.Sentry.getReplay === 'function') { + const replay = window.Sentry.getReplay(); + if (replay && !replay.isEnabled()) { + replay.start(); + } + } + } + return event; + }, beforeSendSpan: tagSpans }); } From 502c36a875e5b40710985b738140414cad0cb5ec Mon Sep 17 00:00:00 2001 From: Tim Hostetler <6970899+thostetler@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:27:11 -0400 Subject: [PATCH 3/4] Integrate Sentry feedback with modal form --- SENTRY_REPLAY_OPTIMIZATION.md | 96 ---------------------- src/index.html | 25 ++++++ src/js/widgets/navbar/widget.js | 140 ++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 96 deletions(-) delete mode 100644 SENTRY_REPLAY_OPTIMIZATION.md diff --git a/SENTRY_REPLAY_OPTIMIZATION.md b/SENTRY_REPLAY_OPTIMIZATION.md deleted file mode 100644 index 5f7e97f37..000000000 --- a/SENTRY_REPLAY_OPTIMIZATION.md +++ /dev/null @@ -1,96 +0,0 @@ -# Sentry Replay Optimization Summary - -## Problem Identified -Your project was burning through 100k replay allowance in 8-10 hours due to high-volume, low-value errors triggering replays unnecessarily. - -## Key Issues Consuming Replay Budget - -Based on analysis of your actual Sentry data, these error patterns were consuming most replays: - -1. **BUMBLEBEE-1TC** - 172,785 events - `SyntaxError: Invalid or unexpected token` -2. **BUMBLEBEE-D** - 13,235 events - RequireJS script loading errors -3. **BUMBLEBEE-7QW/7QV** - 6,787 events - Google Tag Manager `__tcfapi` errors -4. **BUMBLEBEE-1R2** - 7,459 events - Uncompressed asset performance issues -5. **BUMBLEBEE-75Y** - 3,271 events - Search cycle failures (HTTP 429 rate limiting) -6. **BUMBLEBEE-5KN** - 2,584 events - Google Analytics loading issues -7. **Multiple jQuery/Bootstrap issues** - ~5,000 events combined - -## Optimizations Implemented - -### 1. Smart Error Filtering -- **High-volume error patterns**: Added specific regex patterns for your top error types -- **Third-party script filtering**: Filters errors from GTM, GA, Pinterest, etc. -- **RequireJS/Script loading errors**: Filters common module loading failures -- **Network/fetch failures**: Filters transient network issues -- **Performance issues**: Filters asset compression warnings - -### 2. Aggressive Replay Sampling -- **No random session recording**: `replaysSessionSampleRate: 0` -- **Daily quota limits**: 100 replays/day in production, 1000 in development -- **Ultra-low sampling rates**: 5% for critical app errors, 0.1% for others -- **Smart quota management**: Uses localStorage to track daily usage - -### 3. Payload Size Reduction -- **Block all media**: Images/videos not recorded -- **Minimal network capture**: Only critical API endpoints -- **No request/response bodies**: Reduces network payload size -- **Limited DOM mutations**: Caps mutation tracking at 10,000 events -- **Canvas/font optimization**: Disabled canvas recording and font collection -- **5-minute replay limit**: Prevents excessively long recordings - -### 4. Targeted Network Monitoring -Only captures network requests for: -- `/api/v1/` - Core API calls -- `/search/query` - Search queries -- `/resolver/` - Citation resolver -- `/export/` - Export functionality -- `/user/` - User management -- `/myads/` - User libraries - -## Expected Impact -- **~99.9% reduction in replay usage** -- **Elimination of noise from third-party scripts** -- **Focus on genuine application errors** -- **Smaller replay file sizes** -- **Better signal-to-noise ratio for debugging** - -## Optional: Additional CSS Optimizations - -Add these CSS classes to further reduce replay payload: - -```css -/* Block these elements from Sentry replays */ -.sentry-block { - /* Applied to: ads, tracking pixels, large images, videos */ -} - -/* Ignore these elements completely in replays */ -.sentry-ignore { - /* Applied to: analytics widgets, social media embeds */ -} -``` - -Add classes to HTML elements like: -```html - - - - -
-``` - -## Monitoring - -1. **Check daily usage**: Monitor localStorage key `sentry_replay_quota` -2. **Review filtered errors**: Check if important errors are being filtered -3. **Adjust sampling rates**: Fine-tune based on actual usage patterns -4. **Monitor payload sizes**: Ensure replays stay under size limits - -## Next Steps - -1. Deploy these changes and monitor replay usage over 24-48 hours -2. Review which errors are still generating replays in Sentry dashboard -3. Adjust filtering patterns if needed based on new data -4. Consider adding CSS classes to further optimize payload sizes - -This should bring your replay usage down to a sustainable level while maintaining visibility into genuine application issues. \ No newline at end of file diff --git a/src/index.html b/src/index.html index 92a2b09ee..30a0127f0 100644 --- a/src/index.html +++ b/src/index.html @@ -563,6 +563,31 @@

}, beforeSendSpan: tagSpans }); + + const enableFeedbackIntegration = (feedbackFactory) => { + if (typeof feedbackFactory !== 'function') { + return; + } + try { + const integration = feedbackFactory({ + autoInject: false, + colorScheme: 'system', + }); + if (integration && typeof Sentry.addIntegration === 'function') { + Sentry.addIntegration(integration); + } + } catch (_) { + // ignore feedback initialization errors to avoid blocking bootstrap + } + }; + + if (typeof Sentry.lazyLoadIntegration === 'function') { + Sentry.lazyLoadIntegration('feedbackIntegration') + .then(enableFeedbackIntegration) + .catch(() => {}); + } else if (typeof Sentry.feedbackIntegration === 'function') { + enableFeedbackIntegration(Sentry.feedbackIntegration); + } } } diff --git a/src/js/widgets/navbar/widget.js b/src/js/widgets/navbar/widget.js index 45079cf23..1104e9993 100644 --- a/src/js/widgets/navbar/widget.js +++ b/src/js/widgets/navbar/widget.js @@ -361,6 +361,7 @@ define([ submitForm: function($form, $modal) { const submit = () => { + this._sendFeedbackToSentry($form); var data = $form.serialize(); // record the user agent string data += '&user-agent-string=' + encodeURIComponent(navigator.userAgent); @@ -427,6 +428,145 @@ define([ }); }, + _sendFeedbackToSentry: function($form) { + if ( + typeof window.whenSentryReady !== 'function' && + typeof window.Sentry === 'undefined' + ) { + return; + } + + const fields = {}; + const extra = {}; + const skipExtras = new Set([ + 'comments', + 'name', + '_replyto', + '_subject', + '_gotcha', + 'g-recaptcha-response', + ]); + + $form.serializeArray().forEach(({ name, value }) => { + if (!Object.prototype.hasOwnProperty.call(fields, name)) { + fields[name] = value; + } else if (Array.isArray(fields[name])) { + fields[name].push(value); + } else { + fields[name] = [fields[name], value]; + } + + if (!skipExtras.has(name)) { + extra[name] = value; + } + }); + + const message = + typeof fields.comments === 'string' ? fields.comments.trim() : ''; + if (!message) { + return; + } + + const name = + typeof fields.name === 'string' && fields.name.trim() + ? fields.name.trim() + : undefined; + const email = + typeof fields._replyto === 'string' && fields._replyto.trim() + ? fields._replyto.trim() + : undefined; + const urlValue = + typeof fields.url === 'string' && fields.url.trim() + ? fields.url.trim() + : window.location.href; + const feedbackType = + typeof fields['feedback-type'] === 'string' && fields['feedback-type'] + ? fields['feedback-type'] + : 'feedback'; + + const tags = { + feedback_type: feedbackType, + }; + + if (fields.origin) { + tags.feedback_origin = fields.origin; + } + + if (fields.current_page) { + tags.current_page = fields.current_page; + } + + if (fields.current_query) { + tags.current_query = fields.current_query; + } + + if (fields.currentuser) { + tags.current_user = fields.currentuser; + } + + const captureContext = { + tags: _.extend({}, tags), + extra: _.extend( + { + userAgent: navigator.userAgent, + }, + extra + ), + }; + + if (name || email) { + captureContext.user = {}; + if (name) { + captureContext.user.username = name; + } + if (email) { + captureContext.user.email = email; + } + } + + const payload = { + message, + source: 'general-feedback-form', + url: urlValue, + tags, + }; + + if (name) { + payload.name = name; + } + if (email) { + payload.email = email; + } + + const whenReady = + typeof window.whenSentryReady === 'function' + ? window.whenSentryReady() + : Promise.resolve(window.Sentry); + + whenReady + .then((sentry) => { + if (!sentry) { + return; + } + + if (typeof sentry.captureFeedback === 'function') { + try { + sentry.captureFeedback(payload, { captureContext }); + } catch (_) {} + } else if (typeof sentry.sendFeedback === 'function') { + try { + const sendResult = sentry.sendFeedback(payload, { + captureContext, + }); + if (sendResult && typeof sendResult.catch === 'function') { + sendResult.catch(() => {}); + } + } catch (_) {} + } + }) + .catch(() => {}); + }, + navigateToOrcidLink: function() { this._navigate('orcid-page'); }, From b9ddceef67020b7750179d6648701acf2d885610 Mon Sep 17 00:00:00 2001 From: Tim Hostetler <6970899+thostetler@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:39:11 -0400 Subject: [PATCH 4/4] Refactor Sentry bootstrap into config module --- src/config/sentry.js | 294 +++++++++++++++++++++++++++++++ src/index.html | 401 +------------------------------------------ 2 files changed, 295 insertions(+), 400 deletions(-) create mode 100644 src/config/sentry.js diff --git a/src/config/sentry.js b/src/config/sentry.js new file mode 100644 index 000000000..f120593eb --- /dev/null +++ b/src/config/sentry.js @@ -0,0 +1,294 @@ +(function () { + 'use strict'; + + const tagSpans = (span) => { + if (!span || !span.data || !span.data['http.url']) { + return span; + } + const url = new URL(span.data['http.url']); + const params = new URLSearchParams(url.search); + const uiTag = params.get('ui_tag') || params.get('tag'); + if (uiTag) { + span.description = uiTag; + span.data['feature.ui_tag'] = uiTag; + return span; + } + if (params.get('facet') === 'true' && params.has('facet.field')) { + span.description = `${params.get('facet.field')} facet`; + if (params.get('facet.pivot') === 'property,year') { + span.description = 'years graph'; + } + } + if (params.get('stats') === 'true' && params.has('stats.field')) { + const field = params.get('stats.field'); + if (field === 'citation_count') { + span.description = 'citations graph'; + } + if (field === 'read_count') { + span.description = 'reads graph'; + } + } + return span; + }; + + const replayQuotaKey = 'sentry_replay_quota'; + const replayDateKey = 'sentry_replay_date'; + const replayDailyLimit = window.ENV === 'development' ? 1000 : 100; + + const checkDailyReplayQuota = () => { + const today = new Date().toDateString(); + try { + const savedDate = localStorage.getItem(replayDateKey); + let replayCount = parseInt(localStorage.getItem(replayQuotaKey) || '0', 10); + if (savedDate !== today) { + replayCount = 0; + localStorage.setItem(replayDateKey, today); + } + if (replayCount >= replayDailyLimit) { + return false; + } + localStorage.setItem(replayQuotaKey, String(replayCount + 1)); + return true; + } catch (_) { + return Math.random() < 0.0001; + } + }; + + const shouldCaptureReplay = (event) => { + if (window.ENV === 'development') { + return checkDailyReplayQuota(); + } + if (!checkDailyReplayQuota()) { + return false; + } + + const errorMessage = + event?.message || event?.exception?.values?.[0]?.value || ''; + const errorType = event?.exception?.values?.[0]?.type || ''; + const stacktrace = event?.exception?.values?.[0]?.stacktrace?.frames || []; + const filename = stacktrace[0]?.filename || ''; + + const noisyPatterns = [ + /Invalid or unexpected token/i, + /SyntaxError.*Invalid.*token/i, + /Unexpected token/i, + /Script error for.*js\//i, + /RequireJS.*scripterror/i, + /Mismatched anonymous define/i, + /needed by: router/i, + /window\.__tcfapi is not a function/i, + /gtag.*not.*function/i, + /ga.*not.*function/i, + /_gaCustomDimensionUserType.*not defined/i, + /window\.ga.*appear/i, + /gtag.*does not appear to load/i, + /Maximum.*retries.*window\.ga/i, + /jQuery.*not defined/i, + /\$.*not defined/i, + /Bootstrap.*requires jQuery/i, + /Failed to fetch/i, + /TypeError.*fetch/i, + /Network request failed/i, + /Empty values not allowed/i, + /Search cycle failed to start/i, + /Cannot read properties of undefined/i, + /Cannot read properties of null/i, + /pintrk.*not defined/i, + /vbpx.*not defined/i, + /efDataLayer.*not defined/i, + /webVitals.*not defined/i, + /Uncompressed Asset/i, + /Large.*payload/i, + /Large Render Blocking Asset/i, + /ResizeObserver loop limit exceeded/i, + /Non-Error promise rejection/i, + /ChunkLoadError/i, + /Loading chunk.*failed/i, + /Script error/i, + /Maximum call stack size exceeded/i, + /Blocked.*from.*cross-origin/i, + /SecurityError.*cross-origin/i, + /Blocked 'connect'/i, + /Blocked 'script'/i, + /Blocked 'font'/i, + ]; + + const noisyFilenames = [ + /googletagmanager\.com/i, + /google-analytics\.com/i, + /gtm\.js/i, + /analytics\.js/i, + /pinterest\.com/i, + /cubox\.pro/i, + /sentry\.io/i, + /cloudflare\.com/i, + /cdn\./i, + ]; + + if ( + noisyPatterns.some( + (pattern) => pattern.test(errorMessage) || pattern.test(errorType), + ) + ) { + return false; + } + + if (noisyFilenames.some((pattern) => pattern.test(filename))) { + return false; + } + + const criticalErrors = [ + /ViewDestroyedError/i, + /getBeeHive.*getService.*not a function/i, + /Services\.get.*not a function/i, + ]; + + const isCritical = criticalErrors.some( + (pattern) => pattern.test(errorMessage) || pattern.test(errorType), + ); + + const sampleRate = isCritical ? 0.05 : 0.001; + return Math.random() < sampleRate; + }; + + const enableFeedbackIntegration = (feedbackFactory) => { + if (typeof feedbackFactory !== 'function') { + return; + } + try { + const integration = feedbackFactory({ + autoInject: false, + colorScheme: 'system', + }); + if ( + integration && + window.Sentry && + typeof window.Sentry.addIntegration === 'function' + ) { + window.Sentry.addIntegration(integration); + } + } catch (_) { + // ignore feedback initialization errors + } + }; + + const initSentry = () => { + if (typeof window.Sentry === 'undefined') { + return; + } + + window.Sentry.init({ + dsn: 'https://46062cbe0aeb7a3b2bb4c3a9b8cd1ac7@o1060269.ingest.us.sentry.io/4507341192036352', + tracesSampleRate: window.ENV === 'development' ? 1.0 : 0.75, + debug: false, + enableLogs: true, + _experiments: { + enableStandaloneLcpSpans: true, + enableStandaloneClsSpans: true, + }, + allowUrls: [ + /https:\/\/.*\.adsabs\.harvard\.edu\/.*/, + /https:\/\/code\.jquery\.com\/.*/, + /https:\/\/cdn\.jsdelivr\.net\/.*/, + ], + replaysSessionSampleRate: 0, + replaysOnErrorSampleRate: 0, + environment: window.ENV ?? 'production', + integrations: [ + window.Sentry.browserTracingIntegration(), + window.Sentry.replayIntegration({ + maskAllText: false, + blockAllMedia: true, + blockClass: 'sentry-block', + ignoreClass: 'sentry-ignore', + unmask: ['[name=q]', '.search-field', '.query-input'], + networkDetailAllowUrls: [ + /\/api\/v1\//, + /\/search\/query/, + /\/resolver\//, + /\/export\//, + /\/user\//, + /\/myads\//, + ], + networkCaptureBodies: false, + networkRequestHeaders: [], + networkResponseHeaders: [], + maxReplayDuration: 300000, + sessionSampleRate: 0, + errorSampleRate: 0, + recordCanvas: false, + collectFonts: false, + inlineStylesheet: false, + inlineImages: false, + mutationBreadcrumbLimit: 750, + mutationLimit: 10000, + }), + ], + beforeSend(event, hint) { + if (hint.originalException && shouldCaptureReplay(event)) { + if ( + window.Sentry && + typeof window.Sentry.getReplay === 'function' + ) { + const replay = window.Sentry.getReplay(); + if (replay && !replay.isEnabled()) { + replay.start(); + } + } + } + return event; + }, + beforeSendSpan: tagSpans, + }); + + if (window.Sentry && typeof window.Sentry.lazyLoadIntegration === 'function') { + window.Sentry.lazyLoadIntegration('feedbackIntegration') + .then(enableFeedbackIntegration) + .catch(() => {}); + } else if ( + window.Sentry && + typeof window.Sentry.feedbackIntegration === 'function' + ) { + enableFeedbackIntegration(window.Sentry.feedbackIntegration); + } + }; + + const waitForSentry = (callback, errorCallback = () => {}) => { + if (typeof window.Sentry !== 'undefined') { + try { + callback(window.Sentry); + } catch (err) { + errorCallback(err); + } + return; + } + setTimeout(() => waitForSentry(callback, errorCallback), 50); + }; + + window.whenSentryReady = function () { + return new Promise((resolve) => { + if (typeof window.Sentry !== 'undefined') { + resolve(window.Sentry); + } else { + waitForSentry(resolve); + } + }); + }; + + window.getSentry = function (callback, errorCallback = () => {}) { + if (typeof callback !== 'function') { + return; + } + if (typeof window.Sentry !== 'undefined') { + try { + callback(window.Sentry); + } catch (err) { + errorCallback(err); + } + return; + } + waitForSentry(callback, errorCallback); + }; + + window.initSentry = initSentry; +})(); diff --git a/src/index.html b/src/index.html index 30a0127f0..ae5f9ddfb 100644 --- a/src/index.html +++ b/src/index.html @@ -191,408 +191,9 @@

+