1/** 2 * Sentry Browser Integration for Authorize.net Payments 3 * 4 * Captures JavaScript errors, payment form failures, and network issues 5 * Sends them to Sentry for monitoring 6 */ 7(function () { 8 'use strict'; 9 // Sentry Browser Helper - matches server-side implementation 10 window.AnetSentryBrowser = { 11 initialized: false, 12 dsn: null, 13 environment: 'production', 14 breadcrumbs: [], 15 maxBreadcrumbs: 50, 16 /** 17 * Initialize Sentry Browser SDK 18 */ 19 init: function (config) { 20 if (this.initialized) return; 21 this.dsn = config.dsn || null; 22 this.environment = config.environment || 'production'; 23 if (!this.dsn) { 24 console.warn('Sentry DSN not configured - error tracking disabled'); 25 return; 26 } 27 this.initialized = true; 28 }, 29 /** 30 * Check if Sentry is available 31 */ 32 isAvailable: function () { 33 return this.initialized && this.dsn; 34 }, 35 /** 36 * Capture JavaScript exception 37 */ 38 captureException: function (error, context) { 39 if (!this.isAvailable()) return false; 40 try { 41 var eventData = this.buildEventData('error', error.message || error.toString(), error, context); 42 return this.sendToSentry(eventData); 43 } catch (e) { 44 console.error('Failed to capture exception:', e); 45 return false; 46 } 47 }, 48 /** 49 * Capture error message 50 */ 51 captureMessage: function (message, level, context) { 52 if (!this.isAvailable()) return false; 53 try { 54 var eventData = this.buildEventData(level || 'error', message, null, context); 55 return this.sendToSentry(eventData); 56 } catch (e) { 57 console.error('Failed to capture message:', e); 58 return false; 59 } 60 }, 61 /** 62 * Build Sentry event data 63 */ 64 buildEventData: function (level, message, error, context) { 65 context = context || {}; 66 var event = { 67 event_id: this.generateEventId(), 68 timestamp: Math.floor(Date.now() / 1000), 69 level: level, 70 message: message, 71 platform: 'javascript', 72 sdk: { 73 name: 'anet-sentry-browser', 74 version: '1.0.0' 75 }, 76 tags: { 77 plugin: 'authorize-net-payments', 78 environment: this.environment, 79 browser: this.getBrowserInfo(), 80 url: window.location.href, 81 page_type: this.getPageType() 82 }, 83 contexts: { 84 browser: { 85 name: navigator.userAgent, 86 version: navigator.appVersion 87 }, 88 page: { 89 url: window.location.href, 90 referrer: document.referrer 91 } 92 }, 93 extra: { 94 error_message: message 95 } 96 }; 97 // Add exception details with stack trace 98 if (error && error.stack) { 99 event.exception = { 100 values: [{ 101 type: error.name || 'Error', 102 value: error.message || message, 103 stacktrace: { 104 frames: this.parseStackTrace(error.stack) 105 } 106 }] 107 }; 108 } 109 // Add Custome context tags 110 if (context.payment_type) event.tags.payment_type = context.payment_type; 111 if (context.error_type) event.tags.error_type = context.error_type; 112 if (context.order_id) event.tags.order_id = context.order_id; 113 if (context.gateway_id) event.tags.gateway_id = context.gateway_id; 114 if (context.extra) { 115 for (var key in context.extra) { 116 if (context.extra.hasOwnProperty(key)) { 117 event.extra[key] = context.extra[key]; 118 } 119 } 120 } 121 // Add breadcrumbs trail 122 if (this.breadcrumbs.length > 0) { 123 event.breadcrumbs = { 124 values: this.breadcrumbs.slice() 125 }; 126 } 127 return event; 128 }, 129 /** 130 * Add breadcrumb to track events leading to errors 131 */ 132 addBreadcrumb: function (message, category, level, data) { 133 category = category || 'default'; 134 level = level || 'info'; 135 // Limit breadcrumbs to prevent memory issues 136 if (this.breadcrumbs.length >= this.maxBreadcrumbs) { 137 this.breadcrumbs.shift(); // Remove oldest 138 } 139 var breadcrumb = { 140 timestamp: Math.floor(Date.now() / 1000), 141 message: message, 142 category: category, 143 level: level 144 }; 145 if (data) { 146 breadcrumb.data = data; 147 } 148 this.breadcrumbs.push(breadcrumb); 149 }, 150 /** 151 * Clear all breadcrumbs 152 */ 153 clearBreadcrumbs: function () { 154 this.breadcrumbs = []; 155 }, 156 /** 157 * Get all breadcrumbs 158 */ 159 getBreadcrumbs: function () { 160 return this.breadcrumbs; 161 }, 162 /** 163 * Send event to Sentry via HTTP 164 * Updated to work with Visa Sentry using query parameters and envelope format 165 */ 166 sendToSentry: function (eventData) { 167 if (!this.dsn) return false; 168 var dsnParts = this.parseDSN(this.dsn); 169 if (!dsnParts) { 170 console.error('Invalid Sentry DSN'); 171 return false; 172 } 173 // Build envelope format (required by modern Sentry) 174 var timestamp = new Date().toISOString(); 175 var envelope_header = JSON.stringify({ 176 event_id: eventData.event_id, 177 sent_at: timestamp, 178 sdk: { 179 name: 'sentry.javascript.react',
180 version: '10.36.0' 181 } 182 }); 183 var item_header = JSON.stringify({ 184 type: 'event' 185 }); 186 var item_payload = JSON.stringify(eventData); 187 // Combine into envelope format (newline-delimited JSON) 188 var envelopeBody = envelope_header + '\n' + item_header + '\n' + item_payload; 189 // Build endpoint with query parameters (Visa format) 190 var endpoint = `https://${dsnParts.host}${dsnParts.path}/api/${dsnParts.projectId}/envelope/`; 191 var queryParams = `?sentry_version=7&sentry_key=${encodeURIComponent(dsnParts.publicKey)}&sentry_client=${encodeURIComponent('sentry.javascript.react/10.36.0')}`; 192 var fullUrl = endpoint + queryParams; 193 // Use XMLHttpRequest for better browser compatibility 194 var xhr = new XMLHttpRequest(); 195 xhr.open('POST', fullUrl, true); 196 xhr.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8'); 197 xhr.setRequestHeader('Accept', '*/*'); 198 try { 199 xhr.send(envelopeBody); 200 return eventData.event_id; 201 } catch (e) { 202 console.error('Failed to send to Sentry:', e); 203 return false; 204 } 205 }, 206 /** 207 * Parse DSN to extract components 208 * Updated to handle Visa DSN format with path prefix 209 */ 210 parseDSN: function (dsn) { 211 // Match: https://PUBLIC_KEY@HOST/PATH/PROJECT_ID 212 // Example: https://[email protected]/sentry/5 213 var match = dsn.match(/^https?:\/\/([a-f0-9]+)@([^\/]+)(\/[^\/]+)*\/(\d+)\/?$/i); 214 if (!match) return null; 215 return { 216 publicKey: match[1], 217 host: match[2], 218 path: match[3] || '', // e.g., '/sentry' or empty string 219 projectId: match[4] 220 }; 221 }, 222 generateEventId: function () { 223 return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/x/g, function () { 224 return Math.floor(Math.random() * 16).toString(16); 225 }); 226 }, 227 getBrowserInfo: function () { 228 var ua = navigator.userAgent; 229 if (ua.indexOf('Chrome') > -1) return 'Chrome'; 230 if (ua.indexOf('Firefox') > -1) return 'Firefox'; 231 if (ua.indexOf('Safari') > -1) return 'Safari'; 232 if (ua.indexOf('Edge') > -1) return 'Edge'; 233 if (ua.indexOf('MSIE') > -1 || ua.indexOf('Trident') > -1) return 'IE'; 234 return 'Unknown'; 235 }, 236 getPageType: function () { 237 var classList = document.body.classList; 238 if (classList.contains('woocommerce-checkout')) return 'checkout'; 239 if (classList.contains('woocommerce-order-pay')) return 'order-pay'; 240 if (classList.contains('woocommerce-account')) return 'my-account'; 241 return 'unknown'; 242 }, 243 parseStackTrace: function (stack) { 244 var frames = []; 245 var lines = stack.split('\n'); 246 for (var i = 0; i < lines.length; i++) { 247 var line = lines[i].trim(); 248 if (!line) continue; 249 var match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/) || 250 line.match(/(.+?)@(.+?):(\d+):(\d+)/); 251 if (match) { 252 frames.push({ 253 filename: match[2], 254 function: match[1], 255 lineno: parseInt(match[3]), 256 colno: parseInt(match[4]) 257 }); 258 } 259 } 260 return frames.reverse(); 261 } 262 }; 263 // Global error handler 264 window.addEventListener('error', function (event) { 265 if (window.AnetSentryBrowser && window.AnetSentryBrowser.isAvailable()) { 266 window.AnetSentryBrowser.captureException(event.error || new Error(event.message), { 267 error_type: 'unhandled_error', 268 extra: { 269 filename: event.filename, 270 lineno: event.lineno, 271 colno: event.colno 272 } 273 }); 274 } 275 });
276 // Promise rejection handler 277 window.addEventListener('unhandledrejection', function (event) { 278 if (window.AnetSentryBrowser && window.AnetSentryBrowser.isAvailable()) { 279 window.AnetSentryBrowser.captureException( 280 event.reason || new Error('Unhandled Promise Rejection'), 281 { 282 error_type: 'unhandled_rejection', 283 extra: { 284 promise: event.promise 285 } 286 } 287 ); 288 } 289 }); 290 // Automatic breadcrumb tracking for user interactions 291 if (window.AnetSentryBrowser) { 292 // Track page load 293 window.addEventListener('load', function () { 294 if (window.AnetSentryBrowser.isAvailable()) { 295 window.AnetSentryBrowser.addBreadcrumb( 296 'Page loaded: ' + window.location.pathname, 297 'navigation', 298 'info', 299 { 300 url: window.location.href, 301 referrer: document.referrer 302 } 303 ); 304 } 305 }); 306 // Track clicks on important elements 307 document.addEventListener('click', function (event) { 308 if (!window.AnetSentryBrowser || !window.AnetSentryBrowser.isAvailable()) return; 309 var target = event.target; 310 var elementInfo = target.tagName.toLowerCase(); 311 // Add ID or class if available 312 if (target.id) elementInfo += '#' + target.id; 313 else if (target.className && typeof target.className === 'string') elementInfo += '.' + target.className.split(' ')[0]; 314 // Track button clicks 315 if (target.tagName === 'BUTTON' || target.tagName === 'A' || target.type === 'submit') { 316 window.AnetSentryBrowser.addBreadcrumb( 317 'Clicked: ' + elementInfo, 318 'ui.click', 319 'info', 320 { 321 element: elementInfo, 322 text: target.textContent ? target.textContent.substring(0, 50) : '' 323 } 324 ); 325 } 326 }); 327 // Track form submissions 328 document.addEventListener('submit', function (event) { 329 if (!window.AnetSentryBrowser || !window.AnetSentryBrowser.isAvailable()) return; 330 var form = event.target; 331 var formInfo = 'form'; 332 if (form.id) formInfo += '#' + form.id; 333 else if (form.className) formInfo += '.' + form.className.split(' ')[0]; 334 window.AnetSentryBrowser.addBreadcrumb( 335 'Form submitted: ' + formInfo, 336 'ui.submit', 337 'info', 338 { 339 form: formInfo, 340 action: form.action || 'none' 341 } 342 ); 343 }); 344 // Track AJAX/fetch requests 345 if (window.XMLHttpRequest) { 346 var originalOpen = XMLHttpRequest.prototype.open; 347 var originalSend = XMLHttpRequest.prototype.send; 348 XMLHttpRequest.prototype.open = function (method, url) { 349 this._sentryMethod = method; 350 this._sentryUrl = url; 351 return originalOpen.apply(this, arguments); 352 }; 353 XMLHttpRequest.prototype.send = function () { 354 var xhr = this; 355 if (window.AnetSentryBrowser && window.AnetSentryBrowser.isAvailable()) { 356 window.AnetSentryBrowser.addBreadcrumb( 357 xhr._sentryMethod + ' request to ' + xhr._sentryUrl, 358 'http', 359 'info', 360 { 361 method: xhr._sentryMethod, 362 url: xhr._sentryUrl 363 } 364 ); 365 // Track response 366 xhr.addEventListener('load', function () { 367 if (window.AnetSentryBrowser && window.AnetSentryBrowser.isAvailable()) { 368 window.AnetSentryBrowser.addBreadcrumb( 369 'HTTP ' + xhr.status + ' response', 370 'http', 371 xhr.status >= 400 ? 'error' : 'info', 372 { 373 status: xhr.status, 374 url: xhr._sentryUrl 375 } 376 ); 377 } 378 }); 379 } 380 return originalSend.apply(this, arguments); 381 }; 382 } 383 } 384 // Auto-initialize if configuration is available 385 if (typeof anetSentryConfig !== 'undefined' && anetSentryConfig.enabled) { 386 window.AnetSentryBrowser.init({ 387 dsn: anetSentryConfig.dsn, 388 environment: anetSentryConfig.environment 389 }); 390 } 391})(); 392/** 393 * Standard Sentry SDK compatible with code expecting standard Sentry SDK. 394 */ 395(function () { 396 'use strict'; 397 // Create global Sentry object if it doesn't exist. 398 if (typeof window.Sentry === 'undefined') { 399 window.Sentry = { 400 init: function (config) { 401 if (window.AnetSentryBrowser) { 402 window.AnetSentryBrowser.init(config); 403 } 404 }, 405 captureException: function (error, context) { 406 if (window.AnetSentryBrowser) { 407 return window.AnetSentryBrowser.captureException(error, context); 408 } 409 console.warn('Sentry: AnetSentryBrowser not available'); 410 return null; 411 }, 412 captureMessage: function (message, level) { 413 if (window.AnetSentryBrowser) { 414 // Convert Sentry severity levels to our format 415 var sentryLevel = level; 416 if (typeof level === 'object' && level.level) { 417 sentryLevel = level.level; 418 } 419 return window.AnetSentryBrowser.captureMessage(message, sentryLevel); 420 } 421 console.warn('Sentry: AnetSentryBrowser not available'); 422 return null; 423 }, 424 // Add breadcrumb. 425 addBreadcrumb: function (breadcrumb) { 426 if (window.AnetSentryBrowser) { 427 var message = breadcrumb.message || ''; 428 var category = breadcrumb.category || 'default'; 429 var level = breadcrumb.level || 'info'; 430 var data = breadcrumb.data || null; 431 window.AnetSentryBrowser.addBreadcrumb(message, category, level, data); 432 } 433 }, 434 setUser: function (user) { 435 // Store user context for future events. 436 if (window.AnetSentryBrowser) { 437 window.AnetSentryBrowser.userContext = user; 438 } 439 }, 440 // Set tags. 441 setTag: function (key, value) { 442 if (window.AnetSentryBrowser) { 443 if (!window.AnetSentryBrowser.customTags) { 444 window.AnetSentryBrowser.customTags = {}; 445 } 446 window.AnetSentryBrowser.customTags[key] = value; 447 } 448 }, 449 // Set context. 450 setContext: function (name, context) { 451 if (window.AnetSentryBrowser) { 452 if (!window.AnetSentryBrowser.customContexts) { 453 window.AnetSentryBrowser.customContexts = {}; 454 } 455 window.AnetSentryBrowser.customContexts[name] = context; 456 } 457 }, 458 // Check if Sentry is available. 459 isInitialized: function () { 460 return window.AnetSentryBrowser && window.AnetSentryBrowser.isAvailable(); 461 } 462 }; 463 } 464})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.