PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.8
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.8
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / telemetry / sentry-wordpress-integration.php

sentry-wordpress-integration.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.8, at telemetry/sentry-wordpress-integration.php

917 lines 33.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * WordPress-Native Sentry Integration
9 *
10 * Uses WordPress HTTP API to send data directly to Sentry
11 * Compatible with PHP 7.1+ and WordPress 5.0+
12 *
13 * This is the OFFICIAL way to use Sentry without Composer
14 */
15 class MetaSync_Sentry_WordPress {
16
17 private $dsn;
18 private $options;
19 private $environment;
20 private $release;
21 private $public_key;
22 private $secret_key;
23 private $project_id;
24 private $host;
25 private $scheme;
26
27 public function __construct($dsn, $options = []) {
28 $this->dsn = $dsn;
29 $this->options = $options;
30
31 // Use constants for configuration instead of detecting/parsing
32 $this->environment = defined('METASYNC_SENTRY_ENVIRONMENT') ? METASYNC_SENTRY_ENVIRONMENT : $this->detectEnvironment();
33 $this->release = defined('METASYNC_SENTRY_RELEASE') ? METASYNC_SENTRY_RELEASE : (defined('METASYNC_VERSION') ? METASYNC_VERSION : '1.0.0');
34
35 // Handle proxy DSN format (proxy://project_id) or legacy DSN
36 if (strpos($dsn, 'proxy://') === 0) {
37 // New proxy format: proxy://project_id
38 $this->project_id = str_replace('proxy://', '', $dsn);
39 $this->public_key = null; // Not needed for proxy
40 $this->secret_key = null; // Not needed for proxy
41 $this->host = null; // Proxy handles this
42 $this->scheme = 'proxy';
43 } else {
44 // Legacy DSN format for backward compatibility
45 $parsed = parse_url($this->dsn);
46 $this->public_key = isset($parsed['user']) ? $parsed['user'] : null;
47 $this->secret_key = isset($parsed['pass']) ? $parsed['pass'] : null;
48 $this->project_id = trim($parsed['path'], '/');
49 $this->host = isset($parsed['host']) ? $parsed['host'] : null;
50 $this->scheme = isset($parsed['scheme']) ? $parsed['scheme'] : 'https';
51 }
52 }
53
54 /**
55 * Capture an exception and send to Sentry
56 */
57 public function captureException($exception, $extra = []) {
58 $data = $this->formatException($exception, $extra);
59 $result = $this->sendToSentry($data);
60 return is_array($result) ? $result['success'] : $result;
61 }
62
63 /**
64 * Capture a message and send to Sentry
65 */
66 public function captureMessage($message, $level = 'info', $extra = [], $attachment = null) {
67 $data = $this->formatMessage($message, $level, $extra);
68 $result = $this->sendToSentry($data, 'event', $attachment);
69 return is_array($result) ? $result['success'] : $result;
70 }
71
72 /**
73 * Capture user feedback and send to Sentry
74 *
75 * Since Sentry requires user feedback to be associated with an event,
76 * this method first creates an event with the feedback message, then
77 * associates the feedback with that event.
78 *
79 * @param array $feedback Feedback data with keys: name (optional), email (optional), message (required), event_id (optional), severity (optional)
80 * @param array|null $attachment Optional attachment data
81 * @return bool|array Success status, or array with success and event_id
82 */
83 public function captureFeedback($feedback, $attachment = null) {
84 // Get the feedback message
85 $message = '';
86 if (isset($feedback['message']) && !empty($feedback['message'])) {
87 $message = $feedback['message'];
88 } elseif (isset($feedback['comments']) && !empty($feedback['comments'])) {
89 $message = $feedback['comments'];
90 }
91
92 if (empty($message)) {
93 return false;
94 }
95
96 // Get severity level if provided
97 $severity = isset($feedback['severity']) ? sanitize_text_field($feedback['severity']) : '';
98 $valid_severity_levels = array('info', 'warning', 'error', 'fatal');
99
100 // Get otto_pixel_uuid from general options (same way as used in admin handler)
101 $general_options = class_exists('Metasync') ? Metasync::get_option('general') : get_option('metasync_options', [])['general'] ?? [];
102 if (!is_array($general_options)) {
103 $general_options = [];
104 }
105 $project_uuid = isset($general_options['otto_pixel_uuid']) ? sanitize_text_field($general_options['otto_pixel_uuid']) : '';
106
107 // Format event title as "Client Report {UUID}"
108 $event_title = !empty($project_uuid) ? 'Client Report ' . $project_uuid : 'Client Report (UUID Not Configured)';
109
110 // Build the message with title, severity, and user description
111 // Format: "Client Report {uuid}\nSeverity: {level}\n\n{user description}"
112 $formatted_message = $event_title;
113 if (!empty($severity) && in_array($severity, $valid_severity_levels, true)) {
114 $severity_label = ucfirst($severity);
115 $formatted_message .= "\nSeverity: {$severity_label}";
116 }
117
118 // Add attachment indicator if an attachment is present
119 if ($attachment && !empty($attachment['filename'])) {
120 $formatted_message .= "\nAttachment: " . $attachment['filename'];
121 }
122
123 $formatted_message .= "\n\n" . $message;
124
125 // If event_id is already provided, use it directly
126 if (isset($feedback['event_id']) && !empty($feedback['event_id'])) {
127 // Update the feedback message with formatted message (title + severity + description)
128 $feedback['message'] = $formatted_message;
129 $data = $this->formatFeedback($feedback);
130 $result = $this->sendToSentry($data, 'user_report', $attachment);
131 return is_array($result) ? $result['success'] : $result;
132 }
133
134 // Determine event level based on severity
135 $event_level = !empty($severity) && in_array($severity, $valid_severity_levels, true) ? $severity : 'info';
136
137 // Create an event with the formatted message (title + severity + user description)
138 // The title will also be set in culprit field for Sentry to display
139 $event_data = $this->formatMessage($formatted_message, $event_level, [
140 'feedback_source' => 'user_report',
141 'original_feedback' => $feedback,
142 'report_title' => $event_title
143 ]);
144
145 // Set the culprit (title) field for Sentry to display as the event title
146 // The culprit field is what Sentry uses to show the title in the issues list
147 $event_data['culprit'] = $event_title;
148
149 // Add the category tag
150 if (isset($event_data['tags']) && is_array($event_data['tags'])) {
151 $event_data['tags']['category'] = 'user-feedback';
152 } else {
153 $event_data['tags'] = ['category' => 'user-feedback'];
154 }
155
156 // Send the event first with attachment
157 $event_result = $this->sendToSentry($event_data, 'event', $attachment);
158
159 // Extract event_id from response
160 $event_id = null;
161 if (is_array($event_result) && isset($event_result['event_id'])) {
162 $event_id = $event_result['event_id'];
163 } elseif (is_array($event_result) && $event_result['success']) {
164 // If event was sent but no event_id in response, use the one we generated
165 $event_id = $event_data['event_id'] ?? null;
166 }
167
168 // If event creation failed, return false
169 if (!$event_id) {
170 return false;
171 }
172
173 // Now send the feedback with the event_id (no attachment on feedback, it's already on the event)
174 // Make sure the feedback message includes title, severity, and user description
175 $feedback['event_id'] = $event_id;
176 $feedback['message'] = $formatted_message; // Use the formatted message (title + severity + description)
177 $data = $this->formatFeedback($feedback);
178 $feedback_result = $this->sendToSentry($data, 'user_report');
179
180 return is_array($feedback_result) ? $feedback_result['success'] : $feedback_result;
181 }
182
183 /**
184 * Format exception data for Sentry API
185 */
186 private function formatException($exception, $extra = []) {
187 $trace = [];
188 if (is_object($exception) && method_exists($exception, 'getTrace')) {
189 foreach ($exception->getTrace() as $frame) {
190 $trace[] = [
191 'filename' => isset($frame['file']) ? $frame['file'] : '<unknown>',
192 'lineno' => isset($frame['line']) ? $frame['line'] : 0,
193 'function' => isset($frame['function']) ? $frame['function'] : '<unknown>',
194 'module' => isset($frame['class']) ? $frame['class'] : null,
195 'in_app' => $this->isInApp($frame)
196 ];
197 }
198 }
199
200 return [
201 'event_id' => $this->generateEventId(),
202 'timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
203 'level' => 'error',
204 'platform' => 'php',
205 'sdk' => [
206 'name' => 'metasync-wordpress-sentry',
207 'version' => $this->release
208 ],
209 'server_name' => $_SERVER['HTTP_HOST'] ?? 'unknown',
210 'release' => $this->release,
211 'environment' => $this->environment,
212 'exception' => [
213 'values' => [
214 [
215 'type' => is_object($exception) ? get_class($exception) : 'Error',
216 'value' => is_object($exception) ? $exception->getMessage() : (string)$exception,
217 'stacktrace' => ['frames' => array_reverse($trace)]
218 ]
219 ]
220 ],
221 'tags' => $this->getTags(),
222 'extra' => array_merge($this->getSystemContext(), $extra),
223 'user' => $this->getUserContext(),
224 'contexts' => $this->getContexts()
225 ];
226 }
227
228 /**
229 * Format message data for Sentry API
230 */
231 private function formatMessage($message, $level, $extra = []) {
232 return [
233 'event_id' => $this->generateEventId(),
234 'timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
235 'level' => $this->normalizeLevel($level),
236 'platform' => 'php',
237 'sdk' => [
238 'name' => 'metasync-wordpress-sentry',
239 'version' => $this->release
240 ],
241 'server_name' => $_SERVER['HTTP_HOST'] ?? 'unknown',
242 'release' => $this->release,
243 'environment' => $this->environment,
244 'message' => [
245 'message' => $message
246 ],
247 'tags' => $this->getTags(),
248 'extra' => array_merge($this->getSystemContext(), $extra),
249 'user' => $this->getUserContext(),
250 'contexts' => $this->getContexts()
251 ];
252 }
253
254 /**
255 * Format user feedback data for Sentry User Feedback API
256 *
257 * @param array $feedback Feedback data with keys: name, email, message (or comments), event_id
258 * @return array Formatted feedback data
259 */
260 private function formatFeedback($feedback) {
261 // Support both 'message' (JavaScript SDK API) and 'comments' (envelope format)
262 // The JavaScript SDK accepts 'message' but converts it to 'comments' in the envelope
263 $comments = '';
264 if (isset($feedback['message']) && !empty($feedback['message'])) {
265 $comments = $feedback['message'];
266 } elseif (isset($feedback['comments']) && !empty($feedback['comments'])) {
267 $comments = $feedback['comments'];
268 }
269
270 // Validate required fields
271 if (empty($comments)) {
272 throw new InvalidArgumentException('Message/comments field is required for user feedback');
273 }
274
275 // Build feedback payload according to Sentry User Feedback API envelope format
276 // Reference: https://docs.sentry.io/platforms/javascript/user-feedback/#user-feedback-api
277 // IMPORTANT: The envelope format uses 'comments' as the key (not 'message')
278 // The JavaScript SDK accepts 'message' but converts it to 'comments' internally
279 $feedback_data = [
280 'comments' => sanitize_textarea_field($comments)
281 ];
282
283 // Add optional name field
284 if (isset($feedback['name']) && !empty(trim($feedback['name']))) {
285 $feedback_data['name'] = sanitize_text_field($feedback['name']);
286 }
287
288 // Add optional email field
289 if (isset($feedback['email']) && !empty(trim($feedback['email']))) {
290 $feedback_data['email'] = sanitize_email($feedback['email']);
291 }
292
293 // Add event_id if provided (to associate feedback with an event)
294 // Note: In envelope format, event_id can be in the payload
295 if (isset($feedback['event_id']) && !empty($feedback['event_id'])) {
296 $feedback_data['event_id'] = sanitize_text_field($feedback['event_id']);
297 }
298
299 return $feedback_data;
300 }
301
302 /**
303 * Check if the current environment is localhost/development
304 */
305 private function isLocalhost() {
306 $host = parse_url(home_url(), PHP_URL_HOST);
307
308 // Check for common localhost patterns
309 $localhost_patterns = [
310 'localhost',
311 '127.0.0.1',
312 '::1',
313 '0.0.0.0',
314 '.local',
315 '.test',
316 '.dev',
317 '.localhost'
318 ];
319
320 foreach ($localhost_patterns as $pattern) {
321 if (strpos($host, $pattern) !== false) {
322 return true;
323 }
324 }
325
326 // Check if host is an IP address in private ranges
327 if (filter_var($host, FILTER_VALIDATE_IP)) {
328 $ip = ip2long($host);
329 if ($ip !== false) {
330 // Private IP ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
331 if (($ip >= ip2long('10.0.0.0') && $ip <= ip2long('10.255.255.255')) ||
332 ($ip >= ip2long('172.16.0.0') && $ip <= ip2long('172.31.255.255')) ||
333 ($ip >= ip2long('192.168.0.0') && $ip <= ip2long('192.168.255.255'))) {
334 return true;
335 }
336 }
337 }
338
339 return false;
340 }
341
342 /**
343 * Send data to Sentry API using WordPress HTTP functions (proxied through our system with JWT)
344 *
345 * @param array $data Sentry event or feedback data
346 * @param string $item_type Type of item: 'event' or 'user_report'
347 * @param array|null $attachment Optional attachment data
348 * @return bool|array Success status, or array with success and event_id
349 */
350 private function sendToSentry($data, $item_type = 'event', $attachment = null) {
351 // Skip sending to Sentry if running on localhost/development environment
352 if ($this->isLocalhost()) {
353 return false;
354 }
355
356 // Get JWT token for authentication
357 if (!function_exists('metasync_get_jwt_token')) {
358 return false;
359 }
360
361 try {
362 $jwt_token = metasync_get_jwt_token();
363 } catch (Exception $e) {
364 return false;
365 } catch (Error $e) {
366 return false;
367 }
368
369 if (empty($jwt_token)) {
370 return false;
371 }
372
373 // Use WordPress Sentry tunnel endpoint
374 $url = 'https://wordpress.telemetry.infra.searchatlas.com/api/4509950439849985/envelope/';
375
376 // Convert Sentry data to envelope format, including attachment if present
377 $envelope = $this->createSentryEnvelope($data, $item_type, $attachment);
378
379 $plugin_version = defined('METASYNC_VERSION') ? METASYNC_VERSION : '1.0.0';
380
381 $headers = [
382 'Authorization' => 'Bearer ' . $jwt_token,
383 'Content-Type' => 'application/x-sentry-envelope',
384 'X-Plugin-Version' => $plugin_version,
385 'User-Agent' => 'WordPress MetaSync Plugin/' . $plugin_version
386 ];
387
388 // Use cURL directly to ensure proper envelope format
389 $ch = curl_init();
390 curl_setopt($ch, CURLOPT_URL, $url);
391 curl_setopt($ch, CURLOPT_POST, true);
392 curl_setopt($ch, CURLOPT_POSTFIELDS, $envelope);
393 curl_setopt($ch, CURLOPT_HTTPHEADER, array_map(function($key, $value) {
394 return $key . ': ' . $value;
395 }, array_keys($headers), $headers));
396 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
397 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
398 curl_setopt($ch, CURLOPT_TIMEOUT, 5); // 5 second timeout as requested
399 curl_setopt($ch, CURLOPT_USERAGENT, 'WordPress MetaSync Plugin/' . $plugin_version);
400 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5 second connection timeout
401
402 $response = curl_exec($ch);
403 $response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
404 $error = curl_error($ch);
405
406 #curl_close($ch);
407
408 // Log errors in debug mode for troubleshooting
409 if (defined('WP_DEBUG') && WP_DEBUG && WP_DEBUG_LOG) {
410 if ($error) {
411 error_log(sprintf(
412 'MetaSync Sentry Error (%s): %s | Response Code: %s | Item Type: %s',
413 $item_type,
414 $error,
415 $response_code,
416 $item_type
417 ));
418 } elseif ($response_code < 200 || $response_code >= 300) {
419 error_log(sprintf(
420 'MetaSync Sentry HTTP Error (%s): Response Code: %s | Response: %s | Item Type: %s',
421 $item_type,
422 $response_code,
423 substr($response, 0, 200),
424 $item_type
425 ));
426 }
427 }
428
429 // Return false silently on any error or timeout
430 if ($error) {
431 return false;
432 }
433
434 $success = $response_code >= 200 && $response_code < 300;
435
436 // Parse response to extract event_id if available
437 $event_id = null;
438 if ($success && !empty($response)) {
439 $response_data = json_decode($response, true);
440 if (is_array($response_data) && isset($response_data['id'])) {
441 $event_id = $response_data['id'];
442 }
443 }
444
445 // Return array with success status and event_id if available
446 return [
447 'success' => $success,
448 'event_id' => $event_id
449 ];
450 }
451
452 /**
453 * Create Sentry envelope format from event or feedback data
454 *
455 * @param array $data Sentry event or feedback data
456 * @param string $item_type Type of item: 'event' or 'user_report'
457 * @param array|null $attachment Optional attachment data
458 * @return string Envelope format string
459 */
460 private function createSentryEnvelope($data, $item_type = 'event', $attachment = null) {
461 // Envelope header
462 // Note: When sending to the envelope endpoint directly, we don't include DSN
463 // The project ID is already in the URL path
464 $envelope_header = [
465 'sent_at' => gmdate('c')
466 ];
467
468 // Add event_id to header if present (for events) or if it's in feedback data
469 if ($item_type === 'event' && isset($data['event_id'])) {
470 $envelope_header['event_id'] = $data['event_id'];
471 } elseif ($item_type === 'user_report' && isset($data['event_id'])) {
472 // For user feedback, event_id is in the payload, not header
473 // But we can still include it in header if needed
474 }
475
476 // Item header - determine type based on parameter
477 $item_header = [
478 'type' => $item_type
479 ];
480
481 // Create envelope format: header\nitem_header\nitem_payload
482 $envelope = wp_json_encode($envelope_header) . "\n";
483 $envelope .= wp_json_encode($item_header) . "\n";
484 $envelope .= wp_json_encode($data) . "\n";
485
486 // Add attachment if present
487 if ($attachment && isset($attachment['data']) && isset($attachment['filename'])) {
488 // Attachment item header
489 $attachment_header = [
490 'type' => 'attachment',
491 'length' => strlen($attachment['data']),
492 'filename' => $attachment['filename'],
493 'content_type' => $attachment['content_type'] ?? 'application/octet-stream'
494 ];
495
496 // Add attachment to envelope
497 $envelope .= wp_json_encode($attachment_header) . "\n";
498 $envelope .= $attachment['data'] . "\n";
499 }
500
501 return $envelope;
502 }
503
504 /**
505 * Test the Sentry proxy connection
506 *
507 * @return array Test results
508 */
509 public function testProxyConnection() {
510 $test_data = [
511 'message' => [
512 'message' => 'Sentry proxy connection test',
513 'formatted' => 'Sentry proxy connection test'
514 ],
515 'level' => 'info',
516 'timestamp' => gmdate('c'),
517 'platform' => 'php',
518 'sdk' => [
519 'name' => 'metasync-telemetry-test',
520 'version' => '1.0.0'
521 ]
522 ];
523
524 $result = $this->sendToSentry($test_data);
525 $success = is_array($result) ? $result['success'] : $result;
526
527 return [
528 'success' => $success,
529 'message' => $success ? 'Sentry tunnel connection successful' : 'Sentry tunnel connection failed',
530 'endpoint' => 'https://wordpress.telemetry.infra.searchatlas.com/api/4509950439849985/envelope/',
531 'jwt_available' => function_exists('metasync_get_jwt_token') && !empty(metasync_get_jwt_token())
532 ];
533 }
534
535 /**
536 * Test user feedback submission
537 *
538 * @return array Test results
539 */
540 public function testUserFeedback() {
541 $test_feedback = [
542 'name' => 'Test User',
543 'email' => 'test@example.com',
544 'message' => '🧪 Test user feedback submission - ' . gmdate('Y-m-d H:i:s') . ' - This is a test to verify the User Feedback API is working correctly.'
545 ];
546
547 $success = $this->captureFeedback($test_feedback);
548
549 return [
550 'success' => $success,
551 'message' => $success ? 'User feedback test sent successfully' : 'User feedback test failed',
552 'endpoint' => 'https://wordpress.telemetry.infra.searchatlas.com/api/4509950439849985/envelope/',
553 'item_type' => 'user_report',
554 'jwt_available' => function_exists('metasync_get_jwt_token') && !empty(metasync_get_jwt_token())
555 ];
556 }
557
558 /**
559 * Generate Sentry authentication header (legacy - now used for reference only)
560 */
561 private function getSentryAuthHeader() {
562 $timestamp = time();
563 $auth_parts = [
564 'Sentry sentry_version=7',
565 'sentry_client=metasync-wordpress/' . $this->release,
566 'sentry_timestamp=' . $timestamp,
567 'sentry_key=' . $this->public_key
568 ];
569
570 // Note: Modern Sentry DSNs only use public key, no secret key
571 // The secret key is only used for server-side authentication
572 if ($this->secret_key) {
573 $auth_parts[] = 'sentry_secret=' . $this->secret_key;
574 }
575
576 return implode(', ', $auth_parts);
577 }
578
579 /**
580 * Generate unique event ID
581 */
582 private function generateEventId() {
583 return str_replace('-', '', wp_generate_uuid4());
584 }
585
586 /**
587 * Get cached tags for the event (optimized)
588 */
589 private function getTags() {
590 // Cache static tags
591 $cache_key = 'metasync_sentry_tags';
592 $cached_tags = wp_cache_get($cache_key, 'metasync');
593
594 if ($cached_tags !== false) {
595 // Add dynamic tags that change per request
596 $cached_tags['server_name'] = $_SERVER['HTTP_HOST'] ?? 'unknown';
597 return $cached_tags;
598 }
599
600 $tags = [
601 'wp_version' => get_bloginfo('version'),
602 'php_version' => PHP_VERSION,
603 'plugin_version' => $this->release,
604 'environment' => $this->environment,
605 'wp_url' => home_url(),
606 'plugin_name' => 'metasync',
607 // Dynamic tag added per request
608 'server_name' => $_SERVER['HTTP_HOST'] ?? 'unknown'
609 ];
610
611 // Cache for 1 hour
612 wp_cache_set($cache_key, $tags, 'metasync', HOUR_IN_SECONDS);
613
614 return $tags;
615 }
616
617 /**
618 * Get cached system context for Sentry (optimized)
619 */
620 private function getSystemContext() {
621 // Use WordPress object cache to avoid repeated expensive operations
622 $cache_key = 'metasync_system_context';
623 $cached_context = wp_cache_get($cache_key, 'metasync');
624
625 if ($cached_context !== false) {
626 // Add dynamic data that changes per request
627 $cached_context['memory_usage'] = memory_get_usage(true);
628 $cached_context['memory_peak'] = memory_get_peak_usage(true);
629 $cached_context['request_uri'] = $_SERVER['REQUEST_URI'] ?? 'unknown';
630 return $cached_context;
631 }
632
633 // Collect static system context (expensive operations)
634 global $wpdb;
635 $context = [
636 'wp_url' => home_url(),
637 'plugin_version' => $this->release,
638 'plugin_name' => 'Search Engine Labs SEO (MetaSync)',
639 'wordpress_version' => get_bloginfo('version'),
640 'site_title' => get_bloginfo('name'),
641 'site_admin_email' => get_bloginfo('admin_email'),
642 'php_version' => PHP_VERSION,
643 'memory_limit' => ini_get('memory_limit'),
644 'max_execution_time' => ini_get('max_execution_time'),
645 'active_plugins' => count(get_option('active_plugins', [])),
646 'active_theme' => get_template(),
647 'multisite' => is_multisite(),
648 'mysql_version' => method_exists($wpdb, 'get_var') ? $wpdb->get_var('SELECT VERSION()') : 'unknown',
649 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'unknown',
650 // Dynamic data added per request
651 'memory_usage' => memory_get_usage(true),
652 'memory_peak' => memory_get_peak_usage(true),
653 'request_uri' => $_SERVER['REQUEST_URI'] ?? 'unknown'
654 ];
655
656 // Cache static context for 1 hour
657 wp_cache_set($cache_key, $context, 'metasync', HOUR_IN_SECONDS);
658
659 return $context;
660 }
661
662 /**
663 * Get cached contexts for Sentry (optimized)
664 */
665 private function getContexts() {
666 // Cache static context data
667 $cache_key = 'metasync_sentry_contexts';
668 $cached_contexts = wp_cache_get($cache_key, 'metasync');
669
670 if ($cached_contexts !== false) {
671 return $cached_contexts;
672 }
673
674 $contexts = [
675 'runtime' => [
676 'name' => 'php',
677 'version' => PHP_VERSION
678 ],
679 'os' => [
680 'name' => PHP_OS_FAMILY
681 ],
682 'app' => [
683 'app_name' => 'MetaSync Plugin',
684 'app_version' => $this->release
685 ]
686 ];
687
688 // Cache for 1 hour
689 wp_cache_set($cache_key, $contexts, 'metasync', HOUR_IN_SECONDS);
690
691 return $contexts;
692 }
693
694 /**
695 * Get cached user context (anonymized)
696 */
697 private function getUserContext() {
698 // Cache user context since it's based on static site data
699 $cache_key = 'metasync_user_context';
700 $cached_context = wp_cache_get($cache_key, 'metasync');
701
702 if ($cached_context !== false) {
703 return $cached_context;
704 }
705
706 $context = [
707 'id' => substr(md5(home_url() . get_bloginfo('name')), 0, 16),
708 'ip_address' => '{{auto}}' // Let Sentry handle IP detection and anonymization
709 ];
710
711 // Cache for 1 hour
712 wp_cache_set($cache_key, $context, 'metasync', HOUR_IN_SECONDS);
713
714 return $context;
715 }
716
717 /**
718 * Detect current environment
719 */
720 private function detectEnvironment() {
721
722 if (defined('WP_DEBUG') && WP_DEBUG) {
723 return 'development';
724 }
725
726 // Check if running on localhost/development environment
727 if ($this->isLocalhost()) {
728 return 'development';
729 }
730
731 $host = parse_url(home_url(), PHP_URL_HOST);
732 if (strpos($host, 'staging') !== false || strpos($host, 'dev') !== false) {
733 return 'staging';
734 }
735
736 return 'production';
737 }
738
739 /**
740 * Normalize log level for Sentry
741 */
742 private function normalizeLevel($level) {
743 $levels = ['debug', 'info', 'warning', 'error', 'fatal'];
744 return in_array($level, $levels) ? $level : 'info';
745 }
746
747 /**
748 * Check if stack frame is in application code
749 */
750 private function isInApp($frame) {
751 if (!isset($frame['file'])) {
752 return false;
753 }
754
755 $wp_content_dir = defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR : ABSPATH . 'wp-content';
756 return strpos($frame['file'], $wp_content_dir) !== false;
757 }
758
759 /**
760 * Test the connection to Sentry
761 */
762 public function testConnection() {
763 $test_data = $this->formatMessage('🧪 Sentry connection test', 'info', [
764 'test' => true,
765 'timestamp' => time(),
766 'source' => 'connection_test'
767 ]);
768
769 $result = $this->sendToSentry($test_data);
770 $success = is_array($result) ? $result['success'] : $result;
771
772 return [
773 'success' => $success,
774 'dsn_configured' => !empty($this->dsn),
775 'project_id' => $this->project_id,
776 'environment' => $this->environment,
777 'release' => $this->release
778 ];
779 }
780 }
781
782 /**
783 * Global Sentry instance
784 */
785 global $metasync_sentry_wordpress;
786 $metasync_sentry_wordpress = null;
787
788 /**
789 * Initialize Sentry with DSN configuration
790 */
791 function init_metasync_sentry_wordpress() {
792 global $metasync_sentry_wordpress;
793
794 $dsn = '';
795
796 // Use constants defined in metasync.php for configuration
797 if (defined('METASYNC_SENTRY_PROJECT_ID')) {
798 // Create proxy DSN format using the project ID constant
799 $dsn = 'proxy://' . METASYNC_SENTRY_PROJECT_ID;
800 } else {
801 // Fallback: Check wp-config.php for custom DSN (for developers)
802 if (defined('METASYNC_SENTRY_DSN')) {
803 $dsn = METASYNC_SENTRY_DSN;
804 }
805 }
806
807 if (!empty($dsn)) {
808 try {
809 $metasync_sentry_wordpress = new MetaSync_Sentry_WordPress($dsn);
810 return $metasync_sentry_wordpress;
811 } catch (Exception $e) {
812 return null;
813 }
814 } else {
815 return null;
816 }
817 }
818
819 /**
820 * Helper function to capture exceptions
821 */
822 function metasync_sentry_capture_exception($exception, $extra = []) {
823 global $metasync_sentry_wordpress;
824 if (!$metasync_sentry_wordpress) {
825 $metasync_sentry_wordpress = init_metasync_sentry_wordpress();
826 }
827
828 if ($metasync_sentry_wordpress) {
829 return $metasync_sentry_wordpress->captureException($exception, $extra);
830 }
831 return false;
832 }
833
834 /**
835 * Helper function to capture messages
836 */
837 function metasync_sentry_capture_message($message, $level = 'info', $extra = [], $attachment = null) {
838 global $metasync_sentry_wordpress;
839 if (!$metasync_sentry_wordpress) {
840 $metasync_sentry_wordpress = init_metasync_sentry_wordpress();
841 }
842
843 if ($metasync_sentry_wordpress) {
844 return $metasync_sentry_wordpress->captureMessage($message, $level, $extra, $attachment);
845 }
846 return false;
847 }
848
849 /**
850 * Helper function to capture user feedback
851 *
852 * @param array $feedback Feedback data with keys: name (optional), email (optional), message (required), event_id (optional)
853 * @return bool Success status
854 */
855 function metasync_sentry_capture_feedback($feedback, $attachment = null) {
856 global $metasync_sentry_wordpress;
857 if (!$metasync_sentry_wordpress) {
858 $metasync_sentry_wordpress = init_metasync_sentry_wordpress();
859 }
860
861 if ($metasync_sentry_wordpress) {
862 return $metasync_sentry_wordpress->captureFeedback($feedback, $attachment);
863 }
864 return false;
865 }
866
867 /**
868 * Test Sentry connection
869 */
870 function metasync_sentry_test_connection() {
871 global $metasync_sentry_wordpress;
872 if (!$metasync_sentry_wordpress) {
873 $metasync_sentry_wordpress = init_metasync_sentry_wordpress();
874 }
875
876 if ($metasync_sentry_wordpress) {
877 // Use the new proxy test method if available
878 if (method_exists($metasync_sentry_wordpress, 'testProxyConnection')) {
879 return $metasync_sentry_wordpress->testProxyConnection();
880 }
881 // Fallback to legacy method
882 return $metasync_sentry_wordpress->testConnection();
883 }
884
885 return [
886 'success' => false,
887 'error' => 'Sentry not initialized. Check DSN configuration.'
888 ];
889 }
890
891 /**
892 * Test user feedback submission
893 *
894 * @return array Test results
895 */
896 function metasync_sentry_test_user_feedback() {
897 global $metasync_sentry_wordpress;
898 if (!$metasync_sentry_wordpress) {
899 $metasync_sentry_wordpress = init_metasync_sentry_wordpress();
900 }
901
902 if ($metasync_sentry_wordpress) {
903 if (method_exists($metasync_sentry_wordpress, 'testUserFeedback')) {
904 return $metasync_sentry_wordpress->testUserFeedback();
905 }
906 }
907
908 return [
909 'success' => false,
910 'error' => 'Sentry not initialized or test method not available.'
911 ];
912 }
913
914 // Auto-initialize when file is loaded
915 init_metasync_sentry_wordpress();
916 ?>
917