• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom real-time activity logs block for Gutenberg using REST API custom routes

Step-by-Step Guide to building a custom real-time activity logs block for Gutenberg using REST API custom routes

Leveraging WordPress REST API for Real-Time Activity Logs in Gutenberg

For e-commerce platforms built on WordPress, maintaining an up-to-the-minute understanding of user activity is paramount. This includes tracking actions like product views, cart additions, order placements, and even administrative changes. While WordPress offers basic logging, a truly real-time, granular view often requires custom solutions. This guide details how to build a custom Gutenberg block that displays real-time activity logs by interacting with custom REST API routes, providing e-commerce founders and technical managers with a powerful, integrated dashboard component.

Setting Up Custom REST API Routes

The foundation of our real-time system is a custom REST API endpoint that will serve the activity log data. We’ll define a new namespace and a route to fetch these logs. This involves hooking into WordPress’s REST API registration mechanism.

Registering the API Namespace and Route

We’ll use the rest_api_init action hook to register our custom routes. This ensures they are available when the REST API is initialized. We’ll create a route that accepts parameters for pagination and filtering, essential for managing potentially large log datasets.

/**
 * Register custom REST API routes for activity logs.
 */
function register_activity_log_routes() {
    // Register a namespace for our custom API.
    register_rest_namespace( 'my_ecommerce_logs/v1' );

    // Register the route for fetching activity logs.
    register_rest_route( 'my_ecommerce_logs/v1', '/logs', array(
        'methods'  => WP_REST_Server::READABLE, // GET method
        'callback' => 'get_activity_logs_callback',
        'permission_callback' => function() {
            // Ensure only authenticated users with sufficient capabilities can access.
            // Adjust 'manage_options' as per your security requirements.
            return current_user_can( 'manage_options' );
        },
        'args' => array(
            'per_page' => array(
                'default' => 20,
                'type'    => 'integer',
                'min'     => 1,
                'max'     => 100,
                'description' => 'Number of logs to retrieve per page.',
            ),
            'page' => array(
                'default' => 1,
                'type'    => 'integer',
                'min'     => 1,
                'description' => 'The current page of the dataset.',
            ),
            'event_type' => array(
                'required' => false,
                'type'     => 'string',
                'description' => 'Filter logs by event type (e.g., "product_view", "order_placed").',
            ),
        ),
    ) );
}
add_action( 'rest_api_init', 'register_activity_log_routes' );

Callback Function for Fetching Logs

The get_activity_logs_callback function will be responsible for querying the activity log data. For this example, we’ll simulate fetching data from a custom database table or a meta-based storage. In a production environment, you’d replace this with your actual data retrieval logic (e.g., querying a custom table, using WP_Query with custom post types, or retrieving from an external logging service).

/**
 * Callback function to retrieve activity logs.
 *
 * @param WP_REST_Request $request Full data.
 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
 */
function get_activity_logs_callback( WP_REST_Request $request ) {
    $per_page   = absint( $request->get_param( 'per_page' ) );
    $page       = absint( $request->get_param( 'page' ) );
    $event_type = sanitize_text_field( $request->get_param( 'event_type' ) );

    // Simulate fetching logs. In a real scenario, query your database here.
    // Example: Querying a custom table 'wp_activity_logs'.
    global $wpdb;
    $table_name = $wpdb->prefix . 'activity_logs'; // Assuming a custom table exists.

    $offset = ( $page - 1 ) * $per_page;
    $logs_query = "SELECT * FROM {$table_name}";
    $count_query = "SELECT COUNT(*) FROM {$table_name}";

    $where_clauses = array();
    if ( ! empty( $event_type ) ) {
        $where_clauses[] = $wpdb->prepare( "event_type = %s", $event_type );
    }

    if ( ! empty( $where_clauses ) ) {
        $logs_query .= " WHERE " . implode( ' AND ', $where_clauses );
        $count_query .= " WHERE " . implode( ' AND ', $where_clauses );
    }

    $logs_query .= " ORDER BY timestamp DESC LIMIT %d OFFSET %d";
    $logs = $wpdb->get_results( $wpdb->prepare( $logs_query, $per_page, $offset ) );
    $total_logs = $wpdb->get_var( $count_query );

    if ( empty( $logs ) && ! empty( $event_type ) ) {
        // If no logs found for a specific event type, return an empty array.
        $logs = array();
        $total_logs = 0;
    } elseif ( empty( $logs ) && empty( $event_type ) ) {
        // If no logs found at all, return an empty array.
        $logs = array();
        $total_logs = 0;
    } elseif ( $wpdb->last_error ) {
        return new WP_Error( 'db_error', 'Database error occurred while fetching logs.', array( 'status' => 500 ) );
    }

    $response_data = array(
        'logs'       => $logs,
        'total'      => (int) $total_logs,
        'per_page'   => $per_page,
        'current_page' => $page,
    );

    return new WP_REST_Response( $response_data, 200 );
}

Building the Gutenberg Block

Now, we’ll create a custom Gutenberg block that will fetch data from our new REST API endpoint and display it in a user-friendly format. This involves creating a JavaScript file for the block’s editor and frontend rendering, and a PHP file to register the block.

Registering the Gutenberg Block

We need to register our block type using register_block_type. This function takes the path to a block.json file, which defines the block’s metadata, and optionally a directory for its JavaScript and CSS assets.

/**
 * Registers the custom Gutenberg block for activity logs.
 */
function register_activity_log_block() {
    // Ensure the block.json file exists in the same directory as this PHP file.
    register_block_type( __DIR__ );
}
add_action( 'init', 'register_activity_log_block' );

block.json Configuration

The block.json file is crucial for defining the block’s properties, including its name, title, category, and the JavaScript file that will handle its functionality.

{
    "apiVersion": 2,
    "name": "my-ecommerce-logs/activity-log-viewer",
    "title": "Activity Log Viewer",
    "category": "widgets",
    "icon": "chart-line",
    "description": "Displays real-time e-commerce activity logs.",
    "attributes": {
        "logsPerPage": {
            "type": "number",
            "default": 10
        }
    },
    "editorScript": "file:./index.js",
    "editorStyle": "file:./index.css",
    "style": "file:./style.css"
}

JavaScript for Block Editor and Frontend

This JavaScript file will contain the logic for fetching data from our REST API and rendering the logs in both the editor and on the frontend. We’ll use the @wordpress/api-fetch package for making API requests and @wordpress/element for React components.

/**
 * WordPress dependencies
 */
const { registerBlockType } = wp.blocks;
const { apiFetch } = wp;
const { useState, useEffect } = wp.element;
const { InspectorControls } = wp.editor;
const { PanelBody, RangeControl } = wp.components;
const { __ } = wp.i18n;

/**
 * Internal dependencies
 */
import './editor.scss';
import './style.scss';

const ActivityLogViewer = ( { attributes, setAttributes } ) => {
    const { logsPerPage } = attributes;
    const [ logs, setLogs ] = useState( [] );
    const [ isLoading, setIsLoading ] = useState( true );
    const [ error, setError ] = useState( null );
    const [ currentPage, setCurrentPage ] = useState( 1 );
    const [ totalLogs, setTotalLogs ] = useState( 0 );

    const fetchLogs = async ( page = 1 ) => {
        setIsLoading( true );
        setError( null );
        try {
            const response = await apiFetch( {
                path: `/my_ecommerce_logs/v1/logs?per_page=${ logsPerPage }&page=${ page }`,
            } );
            setLogs( response.logs );
            setTotalLogs( response.total );
            setCurrentPage( page );
        } catch ( err ) {
            setError( err.message || __( 'Failed to fetch logs.', 'my-ecommerce-logs' ) );
        } finally {
            setIsLoading( false );
        }
    };

    useEffect( () => {
        fetchLogs( currentPage );
    }, [ logsPerPage, currentPage ] ); // Re-fetch when logsPerPage or currentPage changes.

    const handleNextPage = () => {
        if ( ( currentPage * logsPerPage ) < totalLogs ) {
            fetchLogs( currentPage + 1 );
        }
    };

    const handlePrevPage = () => {
        if ( currentPage > 1 ) {
            fetchLogs( currentPage - 1 );
        }
    };

    const updateLogsPerPage = ( newPerPage ) => {
        setAttributes( { logsPerPage: newPerPage } );
        // Reset to first page when per_page changes to avoid empty results.
        fetchLogs( 1 );
    };

    return (
        <>
            <InspectorControls>
                <PanelBody title={ __( 'Log Settings', 'my-ecommerce-logs' ) }>
                    <RangeControl
                        label={ __( 'Logs Per Page', 'my-ecommerce-logs' ) }
                        value={ logsPerPage }
                        onChange={ updateLogsPerPage }
                        min={ 5 }
                        max={ 50 }
                        step={ 1 }
                    />
                </PanelBody>
            </InspectorControls>

            <div className="activity-log-viewer">
                <h3>{ __( 'Recent Activity Logs', 'my-ecommerce-logs' ) }</h3>
                { isLoading && <p>{ __( 'Loading logs...', 'my-ecommerce-logs' ) }</p> }
                { error && <p className="error">{ error }</p> }
                { !isLoading && !error && logs.length === 0 && <p>{ __( 'No activity logs found.', 'my-ecommerce-logs' ) }</p> }
                { !isLoading && !error && logs.length > 0 && (
                    <table>
                        <thead>
                            <tr>
                                <th>{ __( 'Timestamp', 'my-ecommerce-logs' ) }</th>
                                <th>{ __( 'User', 'my-ecommerce-logs' ) }</th>
                                <th>{ __( 'Event', 'my-ecommerce-logs' ) }</th>
                                <th>{ __( 'Details', 'my-ecommerce-logs' ) }</th>
                            </tr>
                        </thead>
                        <tbody>
                            { logs.map( log => (
                                <tr key={ log.id }>
                                    <td>{ new Date( log.timestamp ).toLocaleString() }</td>
                                    <td>{ log.user_id ? `User ID: ${ log.user_id }` : 'Guest' }</td>
                                    <td>{ log.event_type }</td>
                                    <td>{ log.details }</td>
                                </tr>
                            ) ) }
                        </tbody>
                    </table>
                ) }

                { !isLoading && !error && logs.length > 0 && (
                    <div className="pagination">
                        <button onClick={ handlePrevPage } disabled={ currentPage === 1 }>{ __( 'Previous', 'my-ecommerce-logs' ) }</button>
                        <span>{ __( 'Page', 'my-ecommerce-logs' ) } { currentPage } { __( 'of', 'my-ecommerce-logs' ) } { Math.ceil( totalLogs / logsPerPage ) }</span>
                        <button onClick={ handleNextPage } disabled={ ( currentPage * logsPerPage ) >= totalLogs }>{ __( 'Next', 'my-ecommerce-logs' ) }</button>
                    </div>
                ) }
            </div>
        </>
    );
};

registerBlockType( 'my-ecommerce-logs/activity-log-viewer', {
    edit: ActivityLogViewer,
    save: () => null, // Frontend rendering is handled by the edit component via apiFetch.
} );

Styling the Block

Basic CSS can be added to index.css for the editor and style.css for the frontend to make the log viewer presentable.

/* editor.scss and style.scss */
.activity-log-viewer {
    border: 1px solid #ddd;
    padding: 15px;
    margin-bottom: 20px;
    background-color: #fff;
}

.activity-log-viewer h3 {
    margin-top: 0;
    color: #333;
}

.activity-log-viewer table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 15px;
}

.activity-log-viewer th,
.activity-log-viewer td {
    border: 1px solid #eee;
    padding: 8px;
    text-align: left;
}

.activity-log-viewer th {
    background-color: #f9f9f9;
}

.activity-log-viewer .pagination {
    margin-top: 15px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.activity-log-viewer .pagination button {
    padding: 8px 12px;
    cursor: pointer;
}

.activity-log-viewer .pagination button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.activity-log-viewer .error {
    color: red;
    font-weight: bold;
}

Implementing Real-Time Updates (Polling)

While the above setup provides on-demand fetching, true real-time often implies automatic updates. For this, we can implement a polling mechanism within the JavaScript. This involves setting an interval to re-fetch the logs periodically. Be mindful of the polling interval to avoid excessive server load.

// ... inside the ActivityLogViewer component ...

const POLLING_INTERVAL = 30000; // 30 seconds

useEffect( () => {
    // Start polling
    const intervalId = setInterval( () => {
        fetchLogs( currentPage ); // Re-fetch current page
    }, POLLING_INTERVAL );

    // Cleanup on component unmount
    return () => clearInterval( intervalId );
}, [ currentPage, logsPerPage ] ); // Dependencies ensure polling restarts if page/per_page changes.

// ... rest of the component ...

This polling mechanism will automatically refresh the log data every 30 seconds, giving a near real-time view. For very high-traffic sites, consider WebSockets or Server-Sent Events for more efficient real-time communication, though these require more complex backend infrastructure.

Production Considerations and Enhancements

Data Storage and Performance

Storing activity logs directly in the WordPress database can lead to performance degradation as the tables grow. For production environments, consider:

  • Using a dedicated logging table with appropriate indexes (e.g., on timestamp and event_type).
  • Implementing a log rotation or archival strategy.
  • Offloading logs to a specialized logging service (e.g., Elasticsearch, Splunk, AWS CloudWatch Logs) and having the REST API query that service.

Security

The permission_callback in the REST API route is critical. Ensure that only authorized users can access sensitive activity data. For e-commerce, this typically means users with roles like Administrator or Shop Manager.

Error Handling and User Feedback

The JavaScript includes basic error handling. In a production block, you might want more sophisticated UI feedback for errors, loading states, and empty data scenarios. Displaying the exact error message from the API can also be helpful for debugging.

Filtering and Search

Enhance the block by adding UI elements for filtering logs by date range, user, or specific keywords within the details. This would involve modifying the REST API route to accept more parameters and updating the JavaScript to send these parameters in the apiFetch request.

Conclusion

By combining custom REST API routes with a dynamic Gutenberg block, you can create a powerful, integrated real-time activity log viewer for your WordPress e-commerce site. This solution offers granular insights into user behavior and system events, empowering technical managers and founders to monitor operations effectively and make data-driven decisions. Remember to tailor the data storage, security, and polling strategies to the specific demands of your e-commerce platform.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Advanced Diagnostics: Locating slow Singleton Registry Pattern query bottlenecks in WooCommerce custom checkout pipelines
  • How to construct high-throughput import engines for large member profile directories sets using custom XML/JSON parsers
  • How to design secure Slack Webhooks integration webhook listeners using signature validation and payload queues
  • How to build custom WooCommerce core overrides extensions utilizing modern Heartbeat API schemas
  • Optimizing WooCommerce cart response times by lazy loading custom shipping tracking histories assets

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (42)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (93)
  • WordPress Plugin Development (91)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Advanced Diagnostics: Locating slow Singleton Registry Pattern query bottlenecks in WooCommerce custom checkout pipelines
  • How to construct high-throughput import engines for large member profile directories sets using custom XML/JSON parsers
  • How to design secure Slack Webhooks integration webhook listeners using signature validation and payload queues

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala