WebSocket Message Events Format in law-chain-hot/websocket-devtools

The extension routes all WebSocket activity through a standardized event object containing type, data, messageId, timestamp, source, and frameContext fields that are enriched at each layer of the architecture.

The law-chain-hot/websocket-devtools extension intercepts native browser WebSocket traffic using a three-stage pipeline that relies on a canonical message format to maintain data integrity across contexts. Understanding the exact structure of these WebSocket message events is essential for debugging, extending, or integrating with the tool. This article breaks down the event schema, field semantics, and transmission flow using the actual source implementation from the repository.

Event Structure and Schema

The extension transmits data using two distinct schema variants depending on whether events are sent individually or buffered for performance.

Single Event Format

A solitary WebSocket event travels between components as a JSON object with the following structure:

{
  "type": "websocket-event",
  "data": {
    "type": "message",
    "id": "ws_abc123",
    "direction": "incoming",
    "data": "raw payload or decoded protobuf"
  },
  "messageId": "msg_1657389123456_1_xk7y9",
  "timestamp": 1657389123478,
  "source": "content-script",
  "frameContext": {
    "url": "https://example.com/page",
    "stableId": "https://example.com/page",
    "isIframe": false,
    "frameId": null
  }
}

Batched Event Format

When high-frequency traffic occurs, the injected script buffers multiple events into a single payload to reduce overhead:

{
  "type": "websocket-event-batch",
  "payload": [
    {
      "type": "websocket-event",
      "data": { /* event 1 data */ },
      "messageId": "msg_...",
      "timestamp": 1657389123478
    },
    {
      "type": "websocket-event",
      "data": { /* event 2 data */ },
      "messageId": "msg_...",
      "timestamp": 1657389123480
    }
  ]
}

The content script iterates over the batch, injects missing messageId and timestamp values, and normalizes the source field before forwarding to the background script.

Core Field Definitions

Each field in the event object serves a specific purpose in the routing and display pipeline:

  • type: Identifies the payload variant. Individual events use "websocket-event" while buffered arrays use "websocket-event-batch".
  • data: Contains the raw payload generated by the injected script, including WebSocket direction, connection ID, and message content.
  • messageId: A UUID-like string formatted as msg_<timestamp>_<counter>_<random> generated by the content script to enable deduplication and tracing.
  • timestamp: Milliseconds since epoch (Date.now()) captured when the content script creates the wrapper.
  • source: Normalized to "content-script" after processing to ensure the background script can reliably filter events. The injected script initially uses "websocket-proxy-injected".
  • frameContext: An object containing url, stableId, isIframe, and frameId that identifies the specific frame that generated the event, allowing the DevTools UI to group events per-frame.
  • tabId: Added exclusively by the background script (sender.tab.id) to isolate data between browser tabs.

Cross-Component Communication Flow

The event format evolves as it traverses from the page context to the DevTools panel.

Injected Script (Page Context)

Located in src/content/injected.js, this script intercepts native WebSocket calls and emits events via window.postMessage:

window.postMessage(
  {
    source: "websocket-proxy-injected",
    type: "websocket-event",
    payload: {
      type: "message",
      id: generateConnectionId(),
      direction: "incoming",
      data: rawMessageData
    }
  },
  "*"
);

This occurs at lines 438-447 of the injected script, where the payload contains the raw WebSocket activity before any extension-side enrichment.

Content Script (Extension Context)

The content script (src/content/content.js) listens for these messages and transforms them into the canonical format at lines 1110-1139:

const messageId = generateMessageId();
const messageWithId = {
  type: "websocket-event",
  data: event.data.payload,
  messageId,
  timestamp: Date.now(),
  source: "content-script",
  frameContext: {
    url: window.location.href,
    stableId: (() => {
      try { 
        const u = new URL(window.location.href); 
        return `${u.origin}${u.pathname}`; 
      } catch { 
        return window.location.href; 
      }
    })(),
    isIframe: window !== window.top,
    frameId: window !== window.top ? (() => {
      try { 
        const u = new URL(window.location.href); 
        return `${u.origin}${u.pathname}`; 
      } catch { 
        return window.location.href; 
      }
    })() : null
  }
};

chrome.runtime.sendMessage(messageWithId);

The content script also handles batch normalization at lines 65-73, ensuring every array item has a unique messageId and timestamp before forwarding.

Background Script (Central Hub)

The background script (src/background/background.js) receives the enriched message at lines 31-44, adds the tabId, persists the data, and forwards it to the DevTools UI:

case "websocket-event": {
  message.data.tabId = sender.tab.id;
  message.tabId = sender.tab.id;

  websocketData.connections.push(message.data);
  forwardToDevTools(message);
  sendResponse({ received: true });
  break;
}

The tabId field is critical for the DevTools panel to display only events relevant to the currently inspected tab.

Implementation Examples

Emitting Events from Injected Code

When the proxy intercepts a WebSocket message, it constructs the initial payload:

// src/content/injected.js
window.postMessage({
  source: "websocket-proxy-injected",
  type: "websocket-event",
  payload: {
    type: "message",
    id: connectionId,
    direction: "outgoing",
    data: encodedPayload
  }
}, "*");

Processing Batched Events

For high-throughput scenarios, the content script handles batch arrays by mapping over the payload and injecting missing metadata:

// src/content/content.js
const batchWithIds = batch.map(item => ({
  ...item,
  messageId: item.messageId || generateMessageId(),
  timestamp: item.timestamp || Date.now(),
  source: "content-script"
}));

chrome.runtime.sendMessage({
  type: "websocket-event-batch",
  data: batchWithIds,
  timestamp: Date.now(),
  source: "content-script"
});

Storing Events in Background Memory

The background script maintains an in-memory store of connections, appending each new event to the appropriate connection array:

// src/background/background.js
websocketData.connections.push(message.data);
forwardToDevTools(message);

Summary

  • The canonical event object includes type, data, messageId, timestamp, source, and frameContext fields.
  • Injected scripts emit raw events with source: "websocket-proxy-injected" using window.postMessage.
  • Content scripts normalize events by adding unique IDs, timestamps, and frame context before forwarding via chrome.runtime.sendMessage.
  • Background scripts append tabId to isolate data per tab and forward events to the DevTools panel.
  • Batch processing uses the websocket-event-batch type to optimize high-frequency message flows.

Frequently Asked Questions

What is the exact structure of a WebSocket event message in the extension?

The event is a JSON object with a fixed top-level structure containing type (set to "websocket-event"), data (the WebSocket payload), messageId (unique identifier), timestamp (epoch milliseconds), source (normalized to "content-script"), and frameContext (frame metadata). The background script later adds a tabId field to route the event to the correct DevTools instance.

How does the content script modify events from the injected script?

The content script intercepts messages via window.addEventListener('message', ...) and enriches them by generating a messageId, capturing a timestamp, setting source to "content-script", and constructing a frameContext object containing the current URL and iframe status. This normalization occurs in src/content/content.js before transmission to the background script.

What is the purpose of the frameContext field in WebSocket events?

The frameContext field provides structural metadata about the source frame, including url, stableId, isIframe, and frameId. This allows the DevTools panel to group and filter WebSocket traffic by specific frames within a tab, distinguishing between main document requests and those originating from nested iframes.

Are events sent individually or in batches between components?

Both modes are supported. Individual events use the websocket-event type. For high-frequency traffic, the injected script buffers multiple events and sends them as a websocket-event-batch payload. The content script then decomposes these batches, ensures each item has proper metadata, and forwards them individually to the background script for storage.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →