'boolean', 'sanitize_callback' => static fn( $value ): string => ( '1' === (string) $value || true === $value ) ? '1' : '', 'default' => '', ] ); add_settings_section( self::OBSERVABILITY_SECTION, __( 'UI Observability', 'propertyrightsweb' ), [ self::class, 'render_observability_section_intro' ], self::OPTION_OBSERVABILITY ); add_settings_field( self::OPTION_OBSERVABILITY, __( 'Client-side diagnostics', 'propertyrightsweb' ), [ self::class, 'render_observability_field' ], self::OPTION_OBSERVABILITY, self::OBSERVABILITY_SECTION, [ 'label_for' => self::OPTION_OBSERVABILITY ] ); } /** Section blurb above the opt-in checkbox. */ public static function render_observability_section_intro(): void { echo '
'; esc_html_e( 'Optional, off by default. When enabled, the wp-admin browser reports JavaScript errors and basic performance/diagnostic events to help debug the admin UI. Identifying values are masked client-side before sending; nothing is reported until you turn this on.', 'propertyrightsweb' ); echo '
'; } /** Checkbox that flips OPTION_OBSERVABILITY on/off. Default unchecked (OFF). */ public static function render_observability_field(): void { $enabled = '1' === (string) get_option( self::OPTION_OBSERVABILITY, '' ); ?>'dashboard', self::SLUG_CONTENT => 'content', self::SLUG_OFFERS => 'offers', self::SLUG_ACTIVITY => 'activity', self::SLUG_BUYERS => 'buyers', self::SLUG_WALLET => 'wallet', self::SLUG_SETTINGS => 'settings', self::SLUG_HELP => 'help', ]; public function enqueue( string $hook ): void { if ( ! $this->is_plugin_screen( $hook ) ) { return; } wp_enqueue_style( Prweb_Brand::asset( 'tokens' ), PRWEB_URL . 'assets/css/tokens.css', [], PRWEB_VERSION ); wp_enqueue_style( Prweb_Brand::asset( 'admin' ), PRWEB_URL . 'assets/css/admin.css', [ Prweb_Brand::asset( 'tokens' ) ], PRWEB_VERSION ); // UI observability is OPT-IN and OFF by default (ORL-37 blocker 3). // Only enqueue the SDK bundle — and only feed shared.js a config block // carrying an `endpoint` — when the publisher has turned it on. When // off, the bundle never loads and shared.js's init() guard // (`prweb.observability && prweb.observability.endpoint`) is never met, // so no client-side data is reported. $observability_on = $this->observability_opt_in(); $shared_deps = [ 'jquery', 'wp-i18n' ]; if ( $observability_on ) { // UI observability vendor bundle (vanilla SDK). Loaded as a dependency // of the shared bundle so the SDK is on `window.prwebObservability` // before shared.js runs init(). The SDK lives on its own top-level // global because wp_localize_script emits `var prweb = {...}` between // this bundle and shared.js — nesting under window.prweb would clobber // the SDK. Per-page config rides the `prweb.observability` sub-key. wp_enqueue_script( Prweb_Brand::asset( 'admin-observability' ), PRWEB_URL . 'assets/admin/vendor/ui-observability.iife.js', [], PRWEB_VERSION, true ); $shared_deps[] = Prweb_Brand::asset( 'admin-observability' ); } wp_enqueue_script( Prweb_Brand::asset( 'admin-shared' ), PRWEB_URL . 'assets/admin/shared.js', $shared_deps, PRWEB_VERSION, true ); wp_set_script_translations( Prweb_Brand::asset( 'admin-shared' ), Prweb_Brand::TEXT_DOMAIN, PRWEB_PATH . 'languages' ); // Both arguments carry the brand prefix (ORL-229): the handle via // asset(), the global variable name via js_global(). The second one is // why WP.org's prefix guideline names wp_localize_script() alongside // wp_register_script() — it declares `var prweb = {...}` at global // scope. The bundles under assets/ cannot read JS_GLOBAL, so they // repeat the literal and scripts/check-wp-asset-handles.mjs fails if // the two ever disagree. wp_localize_script( Prweb_Brand::asset( 'admin-shared' ), Prweb_Brand::js_global(), [ 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::NONCE ), 'urls' => [ 'dashboard' => admin_url( 'admin.php?page=' . self::SLUG_DASHBOARD ), 'content' => admin_url( 'admin.php?page=' . self::SLUG_CONTENT ), 'contentDetailBase' => admin_url( 'admin.php?page=' . self::SLUG_CONTENT . '&post=' ), 'offers' => admin_url( 'admin.php?page=' . self::SLUG_OFFERS ), 'activity' => admin_url( 'admin.php?page=' . self::SLUG_ACTIVITY ), 'buyers' => admin_url( 'admin.php?page=' . self::SLUG_BUYERS ), 'wallet' => admin_url( 'admin.php?page=' . self::SLUG_WALLET ), 'settings' => admin_url( 'admin.php?page=' . self::SLUG_SETTINGS ), 'help' => admin_url( 'admin.php?page=' . self::SLUG_HELP ), 'rslXml' => get_rest_url( null, 'pds/v1/rsl' ), 'robotsTxt' => home_url( '/robots.txt' ), ], // Only carries an `endpoint` (the trigger shared.js's init() guards // on) when the publisher has opted in. Off → empty array → no init. 'observability' => $observability_on ? [ 'endpoint' => PRWEB_OBSERVABILITY_PUBLIC_BASE, 'projectId' => 'property-rights-web-admin', 'release' => PRWEB_VERSION, ] : [], ] ); // First-run takeover loads the onboarding bundle instead of the // per-screen script while the wizard is mid-flow. $screen = Prweb_Ajax_Onboarding::wizard_in_progress() ? 'onboarding' : $this->resolve_screen( $hook ); if ( $screen !== null && file_exists( PRWEB_PATH . 'assets/admin/' . $screen . '.js' ) ) { $handle = Prweb_Brand::asset( 'admin-' . $screen ); $deps = [ Prweb_Brand::asset( 'admin-shared' ), 'wp-i18n' ]; // The onboarding bundle's offer-defaults editor and pure // schema/brand/transition helpers live in their own files so // onboarding.js stays under the 500-line cap. Enqueue them // first so window.prwebOnboardingDefaults and // window.prweb.onboardingHelpers are defined before // onboarding.js runs. if ( $screen === 'onboarding' ) { wp_enqueue_script( Prweb_Brand::asset( 'admin-onboarding-helpers' ), PRWEB_URL . 'assets/admin/onboarding-helpers.js', [ 'wp-i18n' ], PRWEB_VERSION, true ); wp_enqueue_script( Prweb_Brand::asset( 'admin-onboarding-defaults' ), PRWEB_URL . 'assets/admin/onboarding-defaults.js', [ Prweb_Brand::asset( 'admin-shared' ), 'wp-i18n' ], PRWEB_VERSION, true ); wp_set_script_translations( Prweb_Brand::asset( 'admin-onboarding-defaults' ), Prweb_Brand::TEXT_DOMAIN, PRWEB_PATH . 'languages' ); $deps[] = Prweb_Brand::asset( 'admin-onboarding-helpers' ); $deps[] = Prweb_Brand::asset( 'admin-onboarding-defaults' ); } // The wallet bundle's auto-payout policy module lives in its own // file so wallet.js stays under the 500-line cap. Enqueue it // first so window.prweb.walletPayouts is defined before // wallet.js runs initWallet(). if ( $screen === 'wallet' ) { wp_enqueue_script( Prweb_Brand::asset( 'admin-wallet-payouts' ), PRWEB_URL . 'assets/admin/wallet-payouts.js', [ Prweb_Brand::asset( 'admin-shared' ), 'wp-i18n' ], PRWEB_VERSION, true ); wp_set_script_translations( Prweb_Brand::asset( 'admin-wallet-payouts' ), Prweb_Brand::TEXT_DOMAIN, PRWEB_PATH . 'languages' ); $deps[] = Prweb_Brand::asset( 'admin-wallet-payouts' ); } wp_enqueue_script( $handle, PRWEB_URL . 'assets/admin/' . $screen . '.js', $deps, PRWEB_VERSION, true ); wp_set_script_translations( $handle, Prweb_Brand::TEXT_DOMAIN, PRWEB_PATH . 'languages' ); if ( $screen === 'onboarding' ) { wp_localize_script( $handle, Prweb_Brand::js_global( 'Wizard' ), [ 'schema' => Prweb_Wizard_Stages::for_localize(), 'brand' => [ 'name' => Prweb_Brand::NAME, 'displayName' => Prweb_Brand::DISPLAY_NAME, ], 'urls' => [ 'rslXml' => get_rest_url( null, 'pds/v1/rsl' ), 'robotsTxt' => home_url( '/robots.txt' ), 'walletPage' => admin_url( 'admin.php?page=' . self::SLUG_WALLET ), 'offersPage' => admin_url( 'admin.php?page=' . self::SLUG_OFFERS ), 'contentPage' => admin_url( 'admin.php?page=' . self::SLUG_CONTENT ), 'settingsPage' => admin_url( 'admin.php?page=' . self::SLUG_SETTINGS ), ], ] ); } } } private function resolve_screen( string $hook ): ?string { if ( 'toplevel_page_' . self::SLUG_DASHBOARD === $hook ) { return 'dashboard'; } foreach ( self::SCREEN_MAP as $slug => $name ) { if ( str_ends_with( $hook, '_page_' . $slug ) ) { // R1.5 — branch the Content screen onto its detail bundle // when the URL carries `?post=