`, server expects `object` keyed by ext name. * The specifiers below force the string/object shapes. * - 35380dcc: nested `content_counts` / `redirect_counts` / * `captured_counts` arrays were flattened to top-level fields. * Listing every flat field as required + `unexpected_field` rejects * the nested re-introduction. * * Automatic vs manual submission is DERIVED from this file, never listed * separately. A field a person typed into a form carries * `'human_typed' => 'message'` (prose they wrote) or * `'human_typed' => 'contact'` (a reply address they gave); a report type * with at least one such field is one a human filled in and clicked to * send, and is therefore worth telling a human about. `error` and * `heartbeat` have none and stay silent. Deriving it here rather than * hardcoding a type list elsewhere is what makes a future fifth type with * a textarea notify without anyone remembering to update the triage side: * declaring the field is the only step. Read by * scripts/feedback-submission-modes.php (consumed in turn by * scripts/error-report-sweep.sh) and locked by * FeedbackSubmissionModeClassificationTest. * * Returns array>> keyed by * report type. Each value is a flat FieldSpec map ready to feed to * ABJ_404_Solution_PayloadSchema::validate(). */ return (function (): array { // Base fields shared by every report type. The producer always // returns these regardless of `$type`; the per-type extras are // unioned in below. $base = [ // Plugin and request metadata 'payload_schema_version' => [ 'type' => 'int', 'enum' => [2], 'description' => 'Additive feedback wire-contract version. Version 2 adds the optional plugin_settings diagnostic snapshot.', ], 'plugin_version' => ['type' => 'string', 'description' => 'ABJ404_VERSION at send time. Empty string is allowed only in early-boot test contexts.'], 'report_type' => ['type' => 'string', 'enum' => ['error', 'heartbeat', 'uninstall', 'support_request']], 'is_uninstall' => ['type' => 'bool', 'description' => 'Back-compat alias for report_type=uninstall. True iff report_type=uninstall.'], 'site_url' => ['type' => 'string', 'description' => 'home_url(). Server GROUP BY key.'], 'locale' => ['type' => 'string'], // Database identity 'db_type' => ['type' => 'string', 'enum' => ['mysql', 'mariadb', 'sqlite', 'other']], 'db_version' => ['type' => 'string'], 'table_prefix' => ['type' => 'string'], // WordPress identity 'wp_version' => ['type' => 'string'], 'is_multisite' => ['type' => 'bool'], 'wp_debug' => ['type' => 'bool'], // PHP runtime identity 'php_version' => ['type' => 'string'], 'server_software' => [ 'type' => 'string', 'description' => 'Server software banner, with the Apache mod_status "Server at Port " footer stripped before transmit and capped to 100 chars. See FeedbackTransport::sanitizeServerSoftware().', ], // Resource limits. Every value is bytes (for size fields) or // seconds (for time fields). Strings like "256M" are a schema // violation: that was bug 4080ffb5 B4. 'resource_limits' => [ 'type' => 'object', 'key_type' => 'string', 'value_type' => 'int', 'description' => 'Bytes for size fields, seconds for time fields. Never ini shorthand strings.', ], 'wp_memory_limit_bytes' => ['type' => 'int|null', 'description' => 'Convenience top-level alias for resource_limits.wp_memory.'], // Extensions and active plugins 'extensions' => [ 'type' => 'object', 'key_type' => 'string', 'value_type' => 'bool', 'description' => '{curl:true, mbstring:true, ...} map. Bug 4080ffb5 B5 shipped this as array; the server rejected it.', ], 'active_plugins' => ['type' => 'array', 'item_type' => 'string'], 'active_theme' => ['type' => 'string', 'description' => '"Name Version" string. Bug 4080ffb5 B5 shipped {name,version} object.'], // Cache identity 'object_cache' => [ 'type' => 'string', 'enum' => ['external', 'default'], 'description' => 'Bug 4080ffb5 B5 shipped this as bool. Server schema is string enum.', ], // Content-count diagnostics. Nullable because the underlying // wp_count_posts / wp_count_terms can fail; null distinguishes // "lookup failed" from "really zero". 'published_posts_count' => ['type' => 'int|null'], 'published_pages_count' => ['type' => 'int|null'], 'categories_count' => ['type' => 'int|null'], 'tags_count' => ['type' => 'int|null'], // Redirect status counts. Flat layout was required by server // schema (commit 35380dcc); the nested layout below the line is // explicitly forbidden by `unexpected_field`. 'redirects_active_total' => ['type' => 'int|null'], 'redirects_manual_count' => ['type' => 'int|null'], 'redirects_automatic_count' => ['type' => 'int|null'], 'redirects_regex_count' => ['type' => 'int|null'], 'redirects_trashed_count' => ['type' => 'int|null'], 'redirects_status_counts_state' => [ 'type' => 'string', 'enum' => ['fresh', 'stale', 'uncomputed', 'unavailable', 'redacted'], 'description' => 'Why the five redirects_* tallies above hold what they hold. The tallies are served from a cache that a background job fills, so NULL/0 alone cannot distinguish "this site has no redirects" from "no count has ever been computed here" (uncomputed) or "the read service could not be reached" (unavailable). `stale` means the counts came from the last-known cache while a refresh is pending.', ], 'redirect_hit_count_histogram' => [ 'type' => 'object|null', 'key_type' => 'string', 'value_type' => 'int', 'description' => 'Aggregate active-redirect hit-count buckets only: zero_hits, one_to_ten_hits, eleven_to_hundred_hits, over_hundred_hits. Never per-redirect URLs.', ], // Captured-404 status counts. 'captured_404s_active_total' => ['type' => 'int|null'], 'captured_404s_new_count' => ['type' => 'int|null'], 'captured_404s_ignored_count' => ['type' => 'int|null'], 'captured_404s_later_count' => ['type' => 'int|null'], 'captured_404s_trashed_count' => ['type' => 'int|null'], 'captured_404s_status_counts_state' => [ 'type' => 'string', 'enum' => ['fresh', 'stale', 'uncomputed', 'unavailable', 'redacted'], 'description' => 'Same discriminator as redirects_status_counts_state, for the five captured_404s_* tallies above.', ], // Log + debug file health. 'log_entries_count' => ['type' => 'int|null'], 'log_table_size_bytes' => ['type' => 'int|null'], 'error_count_in_log' => ['type' => 'int|null'], 'debug_file_size_bytes' => ['type' => 'int|null'], // Optional dev-environment marker. Only present when the // producer detected a dev host; absence on real prod sites is // expected and not a schema violation. 'environment_type' => [ 'type' => 'string', 'enum' => ['development'], 'required' => false, ], // Bruno/Troy diagnostic passthrough. Sites where the redirects // tab times out or rebuild stalls cannot be triaged from typed // columns alone: the binding constraints are MySQL globals // (innodb_buffer_pool_size, tmp_table_size), disk headroom, // and PHP SAPI specifics that the server doesn't pre-declare. // Stored on the server in the JSON passthrough column (the // existing `extras_json` field on the reports row), keyed by // a stable namespace so future probes can land here without // schema changes on either side. Producer-side detail: // FeedbackTransport::environmentExtras(). 'environment_extras' => [ 'type' => 'object', 'key_type' => 'string', // value_type intentionally omitted: this is the JSON // passthrough, mixed scalar/object/array values allowed. 'description' => 'Best-effort site diagnostics: MySQL globals + status counters + session probe, disk free/total, PHP SAPI / opcache (on/off + detail settings), plugin table sizes (with data_free fragmentation), view-build freshness state, active connection count, per-index cardinality, hosting + panel class, object-cache backend, DB charset/collate + per-column collation, WP+PHP timezone, plugin install/upgrade lifecycle, top recurring error signatures, opcache revalidate/validate/cli detail, open_basedir restriction, multisite role + network-activation, .htaccess writability, /tmp filesystem free bytes. Anything new diagnosed for a recurring-user failure goes here first, then optionally graduates to a typed column.', ], 'plugin_settings' => [ 'type' => 'object', 'key_type' => 'string', 'required' => false, 'description' => 'Deny-by-default snapshot of allowlisted behavioral settings. Credentials, emails, free text, and paths are never included.', ], ]; $errorExtras = [ 'error_signature' => ['type' => 'string'], 'previously_sent_line' => ['type' => 'int'], 'debug_log' => [ 'type' => 'string', 'description' => 'Sanitized recent debug-log tail for opted-in error diagnostics. It remains a tail under evidence schema v1; when the named error is older, debug_log_evidence carries a separate non-overlapping anchor and both fields share 262144 bytes.', ], 'debug_log_evidence' => [ 'type' => 'object', 'key_type' => 'string', 'required' => false, 'description' => 'Additive versioned metadata and anchored excerpt for the error named by this report. schema_version=1 preserves debug_log as a tail and allocates one shared 262144-byte budget across the tail and non-overlapping error_excerpt.', ], ]; $heartbeatExtras = [ 'error_signature' => ['type' => 'string'], 'previously_sent_line' => ['type' => 'int'], ]; // `human_typed` marks the fields a PERSON filled in by hand. Only the // three free-text inputs and the reply address qualify: uninstall_reason // is a radio choice, selected_issues is a checkbox join, and // include_diagnostics is a checkbox, none of which is typed prose. // See the note above $supportRequestExtras for what reads this marker. $uninstallExtras = [ 'uninstall_reason' => ['type' => 'string'], 'selected_issues' => ['type' => 'string', 'description' => 'Comma-joined checkbox values from the modal (sanitized server-side).'], 'followup_details' => ['type' => 'string', 'human_typed' => 'message'], 'better_plugin_name' => ['type' => 'string', 'required' => false, 'human_typed' => 'message'], 'other_reason_text' => ['type' => 'string', 'required' => false, 'human_typed' => 'message'], 'contact_email' => ['type' => 'string', 'human_typed' => 'contact'], 'include_diagnostics' => ['type' => 'bool'], 'debug_log' => ['type' => 'string'], ]; // type='support_request' carries 4 user-facing extras on top of the // standard diagnostic base. Sent by Ajax_SupportRequest, which is the // only producer of this type today; the JS form is bound to a fixed // set of trigger surfaces (the "Send support request" button on the // redirects page, the captured-404s page, the plugins-row action, the // settings debug screen, and the corrupt-install fallback screen). // The triggered_from enum is pinned to the producer's allow-list so // a drift in either direction (PHP adds a surface the schema doesn't // list, or JS posts a value PHP did not accept) fails the wire-schema // validator before the server endpoint sees it. Keep this list in // sync with ABJ_404_Solution_Ajax_SupportRequest::ALLOWED_TRIGGER_SOURCES. $supportRequestExtras = [ 'user_message' => ['type' => 'string', 'human_typed' => 'message', 'description' => 'Free-text message from the requester (sanitize_textarea_field, capped at MAX_USER_MESSAGE_LENGTH source-side). Empty string allowed.'], 'reply_email' => ['type' => 'string', 'human_typed' => 'contact', 'description' => 'Optional reply address (sanitize_email). Empty string = anonymous request.'], 'triggered_from' => [ 'type' => 'string', 'enum' => [ 'redirects_page', 'captured_404s_page', 'plugins_row_action', 'settings_debug', 'system_corrupt_install', ], 'description' => 'Which admin surface launched the request. Pinned enum; mirror of Ajax_SupportRequest::ALLOWED_TRIGGER_SOURCES.', ], 'debug_log_excerpt' => ['type' => 'string', 'description' => 'Best-effort sanitized debug-log tail, plus bounded PII-safe AJAX stage-trace JSONL AND AJAX request-checkpoint JSONL AND the detach A/B verdict AND receipt-reconstructed canary interpretation for the sending session, all assembled by SupportEvidenceExcerpt::assemble(), plus the browser transport-attempt buffer drained by SupportRequest.js (timings, byte counts, readyState, protocol; no URL, user text, or identifiers, except a bounded first-200/last-80-character text excerpt retained ONLY when a response body failed to parse as JSON, redacted through PiiRedactor at both ingest boundaries). Empty string when no source is available.'], ]; return [ 'error' => array_merge($base, $errorExtras), 'heartbeat' => array_merge($base, $heartbeatExtras), 'uninstall' => array_merge($base, $uninstallExtras), 'support_request' => array_merge($base, $supportRequestExtras), ]; })();