• 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 custom analytics tracker block for Gutenberg using custom WebAssembly modules

Step-by-Step Guide to building a custom custom analytics tracker block for Gutenberg using custom WebAssembly modules

Setting Up the WebAssembly Environment

To leverage WebAssembly (Wasm) for client-side analytics processing within a Gutenberg block, we first need a robust development environment. This involves setting up a C/C++ compiler toolchain capable of targeting Wasm, such as Emscripten. Emscripten allows us to compile C/C++ code into JavaScript and Wasm, providing a bridge between native code and the browser environment.

Installation typically involves downloading the Emscripten SDK. The most straightforward method is using their `emsdk` script. Ensure you have Git and a compatible C++ compiler (like GCC or Clang) installed on your system.

Installing Emscripten SDK

Clone the Emscripten SDK repository and run the installation script. This process can take some time as it downloads and compiles various dependencies.

Cloning the Repository

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk

Installing and Activating the SDK

./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

The `source ./emsdk_env.sh` command is crucial as it sets up your shell environment to use the Emscripten compiler (`emcc`) and other tools. You’ll need to run this command in every new terminal session where you intend to compile Wasm modules, or add it to your shell’s profile (e.g., `.bashrc`, `.zshrc`).

Developing the Core Analytics Logic in C++

Let’s define a simple C++ module that will perform some basic analytics. For this example, we’ll create a function that counts page views and perhaps tracks a specific event. This logic will eventually be compiled into a Wasm module.

`analytics.cpp` – The C++ Source File

#include <iostream>
#include <string>
#include <vector>
#include <map>

// Global counters for analytics
std::map<std::string, int> page_view_counts;
std::map<std::string, int> event_counts;

// Function to record a page view
extern "C" {
    void record_page_view(const char* page_path) {
        std::string path(page_path);
        page_view_counts[path]++;
        std::cout << "Page view recorded for: " << path << " (Count: " << page_view_counts[path] << ")" << std::endl;
    }

    // Function to record a specific event
    void record_event(const char* event_name) {
        std::string name(event_name);
        event_counts[name]++;
        std::cout << "Event recorded: " << name << " (Count: " << event_counts[name] << ")" << std::endl;
    }

    // Function to get total page views
    int get_total_page_views() {
        int total = 0;
        for (const auto& pair : page_view_counts) {
            total += pair.second;
        }
        return total;
    }

    // Function to get count for a specific page
    int get_page_view_count(const char* page_path) {
        std::string path(page_path);
        if (page_view_counts.count(path)) {
            return page_view_counts[path];
        }
        return 0;
    }

    // Function to get count for a specific event
    int get_event_count(const char* event_name) {
        std::string name(event_name);
        if (event_counts.count(name)) {
            return event_counts[name];
        }
        return 0;
    }

    // Function to reset all analytics data (for testing/debugging)
    void reset_analytics() {
        page_view_counts.clear();
        event_counts.clear();
        std::cout << "Analytics data reset." << std::endl;
    }
}

The `extern “C”` block is crucial. It ensures that the C++ compiler uses C linkage for these functions, making them callable from JavaScript. We’re using standard C++ containers like `std::map` for simplicity, but for performance-critical applications, you might consider more optimized data structures or even raw arrays.

Compiling C++ to WebAssembly

Now, we’ll use Emscripten’s `emcc` compiler to transform our C++ source file into a Wasm module and accompanying JavaScript glue code. This glue code handles the interaction between JavaScript and the Wasm module.

Compilation Command

emcc analytics.cpp -o analytics.js -s WASM=1 -s EXPORTED_FUNCTIONS='["_record_page_view", "_record_event", "_get_total_page_views", "_get_page_view_count", "_get_event_count", "_reset_analytics"]' -s EXPORTED_RUNTIME_METHODS='["cwrap", "ccall"]'

Let’s break down this command:

  • emcc analytics.cpp: Invokes the Emscripten compiler with our source file.
  • -o analytics.js: Specifies the output file name. Emscripten will generate both analytics.wasm and analytics.js.
  • -s WASM=1: Ensures that WebAssembly is used as the compilation target.
  • -s EXPORTED_FUNCTIONS='[...]‘: This is critical. It explicitly lists the C++ functions (prefixed with an underscore) that we want to make accessible from JavaScript.
  • -s EXPORTED_RUNTIME_METHODS='["cwrap", "ccall"]': These methods are part of Emscripten’s runtime and provide convenient ways to call C/C++ functions from JavaScript. cwrap creates a JavaScript wrapper for a C function, and ccall is a more direct way to call functions.

After running this command, you should find two new files in your directory: analytics.wasm (the compiled WebAssembly binary) and analytics.js (the JavaScript glue code).

Creating the Gutenberg Block

Now, we’ll integrate this Wasm module into a custom Gutenberg block. This involves creating the necessary PHP files for the block’s registration and the JavaScript files for its editor interface and frontend rendering.

Block Registration (PHP)

Create a new plugin or add this to your theme’s `functions.php`. We’ll register a server-side block that will enqueue our Wasm-related assets.

`custom-analytics-tracker/custom-analytics-tracker.php`

<?php
/**
 * Plugin Name: Custom Analytics Tracker
 * Description: A Gutenberg block with custom WebAssembly analytics.
 * Version: 1.0.0
 * Author: Your Name
 */

function custom_analytics_tracker_register_block() {
    // Automatically load dependencies and version
    $asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php');

    wp_register_script(
        'custom-analytics-tracker-editor-script',
        plugins_url( 'build/index.js', __FILE__ ),
        $asset_file['dependencies'],
        $asset_file['version']
    );

    wp_register_style(
        'custom-analytics-tracker-editor-style',
        plugins_url( 'build/index.css', __FILE__ ),
        array( 'wp-edit-blocks' ),
        $asset_file['version']
    );

    register_block_type( 'custom-analytics-tracker/analytics-block', array(
        'editor_script' => 'custom-analytics-tracker-editor-script',
        'editor_style'  => 'custom-analytics-tracker-editor-style',
        'render_callback' => 'custom_analytics_tracker_render_block',
    ) );
}
add_action( 'init', 'custom_analytics_tracker_register_block' );

function custom_analytics_tracker_render_block( $attributes ) {
    // Enqueue Wasm assets for the frontend
    wp_enqueue_script(
        'custom-analytics-wasm-runtime',
        plugins_url( 'dist/analytics.js', __FILE__ ),
        array(), // No dependencies for the JS glue code itself
        '1.0.0',
        true // Load in footer
    );
    // The .wasm file will be loaded by the .js file automatically

    // You might want to pass some initial data or configuration here
    // For now, we'll just return an empty div that the JS will populate/manage
    return '<div id="custom-analytics-block-output"></div>';
}

// Function to enqueue Wasm assets for the editor as well
function custom_analytics_tracker_enqueue_wasm_editor_assets() {
    wp_enqueue_script(
        'custom-analytics-wasm-editor-runtime',
        plugins_url( 'dist/analytics.js', __FILE__ ),
        array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
        '1.0.0',
        true
    );
}
add_action( 'enqueue_block_editor_assets', 'custom_analytics_tracker_enqueue_wasm_editor_assets' );

In this PHP file:

  • We register the block type `custom-analytics-tracker/analytics-block`.
  • We specify the editor script (`build/index.js`) and style (`build/index.css`). These are typically generated by a build process (e.g., using `@wordpress/scripts`).
  • A `render_callback` is defined (`custom_analytics_tracker_render_block`). This function is responsible for rendering the block’s HTML on the frontend. Crucially, it enqueues our compiled Wasm JavaScript glue code (`dist/analytics.js`).
  • We also enqueue the Wasm runtime script specifically for the block editor using `enqueue_block_editor_assets`.

Gutenberg Block JavaScript (Editor & Frontend)

We’ll use `@wordpress/scripts` for building our block’s JavaScript. This toolchain handles JSX compilation, module bundling, and more. First, set up your `package.json`.

`custom-analytics-tracker/package.json`

{
  "name": "custom-analytics-tracker",
  "version": "1.0.0",
  "description": "A Gutenberg block with custom WebAssembly analytics.",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "keywords": ["wordpress", "gutenberg", "webassembly", "analytics"],
  "author": "Your Name",
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@wordpress/scripts": "^26.0.0"
  }
}

Install the dependencies:

cd custom-analytics-tracker
npm install

Now, create the main JavaScript file for your block. This file will handle both the editor interface and the frontend logic, including loading and interacting with the Wasm module.

`custom-analytics-tracker/src/index.js`

/**
 * WordPress dependencies
 */
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, Button, Placeholder } from '@wordpress/components';

/**
 * Internal dependencies
 */
import './style.scss'; // For frontend styles
import './editor.scss'; // For editor styles

// Placeholder for Wasm module loading status and instance
let wasmModule = null;
let isWasmLoaded = false;
const wasmModulePromise = new Promise((resolve, reject) => {
    // Ensure the script is loaded before trying to access it
    // The PHP enqueue should handle this, but a fallback is good.
    if (window.Module && window.Module._record_page_view) {
        resolve(window.Module);
    } else {
        // Fallback: attempt to find the script if not immediately available
        const script = document.querySelector('script[src*="analytics.js"]');
        if (script) {
            script.onload = () => {
                if (window.Module && window.Module._record_page_view) {
                    resolve(window.Module);
                } else {
                    reject(new Error('Wasm module not initialized after script load.'));
                }
            };
            script.onerror = () => reject(new Error('Failed to load Wasm runtime script.'));
        } else {
            reject(new Error('Wasm runtime script not found.'));
        }
    }
});

wasmModulePromise.then((module) => {
    wasmModule = module;
    isWasmLoaded = true;
    console.log('WebAssembly Analytics Module loaded successfully.');
}).catch((error) => {
    console.error('Error loading WebAssembly Analytics Module:', error);
    isWasmLoaded = false;
});

// Helper function to call Wasm functions
function callWasmFunction(functionName, args = [], returnType = 'void') {
    if (!isWasmLoaded || !wasmModule) {
        console.error('Wasm module not loaded or initialized.');
        return null;
    }
    try {
        // Use cwrap for safer function calls
        const func = wasmModule.cwrap(functionName, returnType, args.map(arg => typeof arg));
        return func(...args);
    } catch (e) {
        console.error(`Error calling Wasm function ${functionName}:`, e);
        return null;
    }
}

// --- Block Registration ---
registerBlockType('custom-analytics-tracker/analytics-block', {
    title: __('Custom Analytics Tracker', 'custom-analytics-tracker'),
    icon: 'chart-bar',
    category: 'widgets',

    attributes: {
        pagePath: {
            type: 'string',
            default: window.location.pathname, // Default to current page path
        },
        eventName: {
            type: 'string',
            default: '',
        },
    },

    edit: ({ attributes, setAttributes }) => {
        const { pagePath, eventName } = attributes;
        const [totalViews, setTotalViews] = useState(0);
        const [specificPageViews, setSpecificPageViews] = useState(0);
        const [specificEventCount, setSpecificEventCount] = useState(0);
        const [statusMessage, setStatusMessage] = useState('');

        // Effect to load Wasm data when the component mounts or dependencies change
        useEffect(() => {
            if (isWasmLoaded) {
                const currentPath = pagePath || window.location.pathname;
                const total = callWasmFunction('get_total_page_views', [], 'number');
                const pageCount = callWasmFunction('get_page_view_count', [currentPath], 'number');
                const eventCount = eventName ? callWasmFunction('get_event_count', [eventName], 'number') : 0;

                setTotalViews(total || 0);
                setSpecificPageViews(pageCount || 0);
                setSpecificEventCount(eventCount || 0);
                setStatusMessage('Analytics data loaded.');
            } else {
                setStatusMessage('Waiting for WebAssembly module to load...');
            }
        }, [isWasmLoaded, pagePath, eventName]); // Re-run if Wasm loads or attributes change

        const handleRecordPageView = () => {
            if (!pagePath) {
                setStatusMessage('Please set a page path.');
                return;
            }
            callWasmFunction('record_page_view', [pagePath]);
            setStatusMessage(`Page view recorded for: ${pagePath}`);
            // Optionally refresh counts immediately
            setSpecificPageViews(prev => prev + 1);
            setTotalViews(prev => prev + 1);
        };

        const handleRecordEvent = () => {
            if (!eventName) {
                setStatusMessage('Please set an event name.');
                return;
            }
            callWasmFunction('record_event', [eventName]);
            setStatusMessage(`Event recorded: ${eventName}`);
            // Optionally refresh counts immediately
            setSpecificEventCount(prev => prev + 1);
        };

        const handleReset = () => {
            callWasmFunction('reset_analytics');
            setTotalViews(0);
            setSpecificPageViews(0);
            setSpecificEventCount(0);
            setStatusMessage('Analytics data reset.');
        };

        return (
            <div className="custom-analytics-block-editor">
                {!isWasmLoaded && (
                    <Placeholder
                        icon="chart-bar"
                        label={__('Custom Analytics Tracker', 'custom-analytics-tracker')}
                        instructions={__('Loading WebAssembly module...', 'custom-analytics-tracker')}
                    />
                )}
                {isWasmLoaded && (
                    <>
                        <InspectorControls>
                            <PanelBody title={__('Analytics Settings', 'custom-analytics-tracker')} initialOpen={true}>
                                <TextControl
                                    label={__('Page Path to Track', 'custom-analytics-tracker')}
                                    value={pagePath}
                                    onChange={(newPath) => setAttributes({ pagePath: newPath })}
                                    help={__('Enter the path for page view tracking.', 'custom-analytics-tracker')}
                                />
                                <Button isPrimary onClick={handleRecordPageView}>
                                    {__('Record Page View', 'custom-analytics-tracker')}
                                </Button>
                                <hr />
                                <TextControl
                                    label={__('Event Name', 'custom-analytics-tracker')}
                                    value={eventName}
                                    onChange={(newEventName) => setAttributes({ eventName: newEventName })}
                                    help={__('Enter the name for event tracking.', 'custom-analytics-tracker')}
                                />
                                <Button isPrimary onClick={handleRecordEvent}>
                                    {__('Record Event', 'custom-analytics-tracker')}
                                </Button>
                                <hr />
                                <Button isDestructive onClick={handleReset}>
                                    {__('Reset All Analytics Data', 'custom-analytics-tracker')}
                                </Button>
                            </PanelBody>
                        </InspectorControls>
                        <div className="custom-analytics-block-content">
                            <h3>{__('Analytics Dashboard (Editor)', 'custom-analytics-tracker')}</h3>
                            <p><strong>{__('Total Page Views:', 'custom-analytics-tracker')}</strong> {totalViews}</p>
                            <p><strong>{__('Views for', 'custom-analytics-tracker')} {pagePath || 'N/A'}:</strong> {specificPageViews}</p>
                            <p><strong>{__('Count for Event', 'custom-analytics-tracker')} {eventName || 'N/A'}:</strong> {specificEventCount}</p>
                            <p><small>{statusMessage}</small></p>
                        </div>
                    </>
                )}
            </div>
        );
    },

    save: ({ attributes }) => {
        const { pagePath, eventName } = attributes;

        // On the frontend, we want to trigger the Wasm functions based on page load or user interaction.
        // The PHP render_callback already enqueues the Wasm JS.
        // We'll use useEffect here to ensure the Wasm module is ready before attempting to call functions.

        useEffect(() => {
            if (isWasmLoaded) {
                // Record page view on load if pagePath is set
                if (pagePath) {
                    callWasmFunction('record_page_view', [pagePath]);
                }
                // Record event on load if eventName is set
                if (eventName) {
                    callWasmFunction('record_event', [eventName]);
                }
            } else {
                // If Wasm isn't loaded yet, try again when it is
                const interval = setInterval(() => {
                    if (isWasmLoaded) {
                        if (pagePath) callWasmFunction('record_page_view', [pagePath]);
                        if (eventName) callWasmFunction('record_event', [eventName]);
                        clearInterval(interval);
                    }
                }, 100); // Check every 100ms
                return () => clearInterval(interval); // Cleanup interval
            }
        }, [isWasmLoaded, pagePath, eventName]); // Re-run if Wasm loads or attributes change

        // The save function should return static HTML.
        // The actual analytics data display will be handled by JS on the frontend,
        // or we can render a placeholder and let JS update it.
        // For simplicity, we'll return a div that JS can target.
        return (
            <div className="custom-analytics-block-frontend">
                <p>{__('Analytics tracking active.', 'custom-analytics-tracker')}</p>
                {/* This div can be targeted by JS to display frontend analytics */}
                <div id="custom-analytics-block-frontend-output"></div>
            </div>
        );
    },
});

In this JavaScript file:

  • We import necessary Gutenberg components.
  • We define a promise (`wasmModulePromise`) to handle the asynchronous loading of our Wasm module. The Emscripten-generated `analytics.js` script, when loaded, exposes a global `Module` object.
  • The `callWasmFunction` helper abstracts the interaction with the Wasm module using `cwrap`.
  • The `edit` function renders the block’s interface in the Gutenberg editor. It uses `useState` and `useEffect` to fetch and display analytics data. It also includes `InspectorControls` for setting the page path and event name, and buttons to trigger Wasm functions.
  • The `save` function returns static HTML for the frontend. It also includes a `useEffect` hook to trigger Wasm functions when the component mounts on the frontend. Note that the `save` function should ideally return HTML that reflects the *initial* state or a placeholder, as dynamic updates are best handled client-side.

Build Process and File Structure

After setting up your `package.json` and `src/index.js`, you need to run the build process. This will compile your JavaScript (including JSX) and SCSS files into the `build/` directory, creating `index.js` and `index.css`.

Running the Build

cd custom-analytics-tracker
npm run build

You also need to place your compiled Wasm files (`analytics.js` and `analytics.wasm`) in a `dist/` directory within your plugin folder. This is where the PHP `plugins_url()` function expects them.

Expected File Structure

custom-analytics-tracker/
├── build/
│   ├── index.js
│   ├── index.css
│   └── index.asset.php  <-- Generated by @wordpress/scripts
├── dist/
│   ├── analytics.js
│   └── analytics.wasm
├── src/
│   ├── index.js
│   ├── style.scss
│   └── editor.scss
├── custom-analytics-tracker.php
└── package.json

Testing and Debugging

To test, activate your plugin in WordPress. Add the “Custom Analytics Tracker” block to a post or page. Open the browser’s developer console to see log messages from both JavaScript and the Wasm module (via `std::cout`).

Browser Developer Console

You should see messages like:

WebAssembly Analytics Module loaded successfully.
Page view recorded for: /your-test-page (Count: 1)
Analytics data loaded.

If you encounter issues:

  • Wasm Module Not Loading: Double-check that `analytics.js` and `analytics.wasm` are correctly placed in the `dist/` folder and that the PHP `plugins_url()` paths are accurate. Verify that `wp_enqueue_script` is correctly called for both the editor and frontend. Check the browser console for network errors related to loading `analytics.js` or `analytics.wasm`.
  • Function Call Errors: Ensure the function names in `EXPORTED_FUNCTIONS` in `emcc` command match the C++ function names (with `_` prefix). Verify the argument types and return types specified in `cwrap` or `ccall` are correct.
  • C++ Logic Errors: Debug your C++ code independently. You can compile it to a standalone JavaScript/Wasm application using Emscripten to test its core logic before integrating it into WordPress.

Advanced Considerations

This setup provides a foundation. For production systems, consider:

  • Performance Optimization: Profile the Wasm module’s execution. For complex computations, ensure efficient data structures and algorithms are used in C++.
  • Data Serialization: For passing complex data structures between JavaScript and Wasm, you might need to implement custom serialization/deserialization logic or use Emscripten’s memory management features.
  • Error Handling: Implement more robust error handling in both C++ and JavaScript, potentially returning error codes or using exceptions that can be caught by the JavaScript glue code.
  • State Management: For more complex blocks, consider using a state management library (like Redux or Zustand) in your block’s JavaScript to manage the interaction with the Wasm module and UI updates.
  • Security: While Wasm runs in a sandboxed environment, be mindful of any sensitive data processed. Ensure your C++ code doesn’t introduce vulnerabilities.
  • Build Pipeline Integration: Integrate the Emscripten compilation step into your main WordPress build process (e.g., using Webpack or Gulp) for a streamlined development workflow.

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

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

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

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

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