Beagle’s plugin architecture separates log data from log presentation. Each log type has a corresponding plugin that knows how to display it in the inspector. This design makes Beagle highly extensible and allows custom log types with custom UI.
BeagleLogPlugin Abstract Class
All plugins extend the BeagleLogPlugin abstract class, which defines the contract for rendering logs:
export abstract class BeagleLogPlugin < T extends BeagleLog > {
abstract name : string ;
abstract canHandle ( log : BeagleLog ) : log is T ;
abstract provideDetailContent ( log : T ) : DetailContent ;
provideCardFooter ( _ : T ) : Content | BoxContent | null {
return null ;
}
exportToJSON ( log : T ) : string {
return JSON . stringify ( log );
}
}
Required Properties and Methods
A unique identifier for the plugin. Used for debugging and display purposes. Example: 'Message', 'Error', 'Networking'
canHandle(log: BeagleLog): log is T
A type guard that determines if this plugin can handle a given log. Beagle uses this to route logs to the correct plugin. Returns: Boolean type predicate that narrows the log type to TExample: canHandle ( log : BeagleLog ): log is MessageLog {
return log instanceof MessageLog ;
}
provideDetailContent(log: T): DetailContent
Generates the detailed view content when a log is expanded in the inspector. This is where you define what information is shown and how it’s laid out. Returns: A DetailContent object describing the UI structure
Optional Methods
provideCardFooter(log: T): Content | BoxContent | null
exportToJSON(log: T): string
Defines how the log should be exported. The default implementation uses JSON.stringify(log). Override this to customize export format or exclude sensitive data.
How Plugins Work
The plugin system follows this flow:
1. Plugin Registration
Plugins are registered during app initialization:
Beagle . registerPlugin ( new MessageLogPlugin ());
Beagle . registerPlugin ( new ErrorLogPlugin ());
Internally, this adds the plugin to a static array:
static registerPlugin ( plugin : BeagleLogPlugin < BeagleLog > ) {
this . plugins . push ( plugin );
}
2. Log Routing
When a log is displayed, Beagle finds its plugin:
static findPlugin < T extends BeagleLog >( log : T ) : BeagleLogPlugin < T > {
const logName = log . constructor . name ;
// Check cache first
if ( this . cache . has ( logName )) {
return this . cache . get ( logName ) as BeagleLogPlugin < T >;
}
// Find matching plugin
const logPlugin = this . plugins . find (( plugin ) => plugin . canHandle ( log ));
if ( ! logPlugin ) {
throw new Error ( `No plugin found for log: ${ typeof log } ${ logName } ` );
}
// Cache for next time
this . cache . set ( logName , logPlugin );
return logPlugin as BeagleLogPlugin < T >;
}
Plugin lookups are cached by log constructor name, so each log type only searches plugins once.
3. Content Generation
The plugin’s methods are called to render the log:
List view: provideCardFooter() for additional card content
Detail view: provideDetailContent() for the expanded view
Export: exportToJSON() when exporting logs
Built-in Plugins
Beagle includes three built-in plugins that demonstrate different rendering strategies:
MessageLogPlugin
The simplest plugin, displaying just the message text:
export class MessageLogPlugin extends BeagleLogPlugin < MessageLog > {
name : string = 'Message' ;
canHandle ( log : BeagleLog ) : log is MessageLog {
return log instanceof MessageLog ;
}
provideDetailContent ( log : BeagleLog ) : DetailContent {
return {
key: 'message' ,
kind: 'list' ,
children: [
{
kind: 'text' ,
text: log . message ,
selectable: true ,
},
],
};
}
}
Key features:
Minimal implementation
Makes text selectable for copying
No footer content
ErrorLogPlugin
Displays error details with stack traces:
export class ErrorLogPlugin extends BeagleLogPlugin < ErrorLog > {
name : string = 'Error' ;
canHandle ( log : BeagleLog ) : log is ErrorLog {
return log instanceof ErrorLog ;
}
provideDetailContent ({ error , message } : ErrorLog ) : DetailContent {
if ( ! ( error instanceof Error )) {
return {
key: 'error' ,
kind: 'list' ,
children: [
{
kind: 'text' ,
text: message ,
selectable: true ,
},
],
};
}
const listItems : Content [] = [
{ kind: 'label' , label: 'Name' , value: error . name },
{
kind: 'label' ,
label: 'Message' ,
value: error . message ,
},
];
if ( error . stack ) {
listItems . push ({
kind: 'text' ,
text: 'Stack' ,
variant: 'body' ,
bold: true ,
});
listItems . push ({ kind: 'text' , text: error . stack , selectable: true });
}
if ( error . cause ) {
listItems . push ({
kind: 'text' ,
text: 'Cause' ,
variant: 'body' ,
bold: true ,
selectable: true ,
});
listItems . push ({
kind: 'text' ,
text: error . cause ?. toString () ?? '' ,
selectable: true ,
});
}
return {
key: 'error' ,
kind: 'list' ,
children: listItems ,
};
}
provideCardFooter ( log : ErrorLog ) : Content | BoxContent | null {
if ( ! ( log . error instanceof Error )) {
return null ;
}
return {
kind: 'text' ,
text: log . error . stack ?? '' ,
variant: 'caption' ,
lines: 2 ,
};
}
}
Key features:
Handles both Error objects and primitive errors
Shows error name, message, stack, and cause
Preview stack trace in card footer (2 lines)
All error details selectable for copying
NetworkingLogPlugin
The most complex plugin, with tabbed interface and rich formatting:
export class NetworkingLogPlugin extends BeagleLogPlugin < NetworkingLog > {
name : string = 'Networking' ;
canHandle ( log : BeagleLog ) : log is NetworkingLog {
return log instanceof NetworkingLog ;
}
provideCardFooter ( log : NetworkingLog ) : BoxContent {
const children : Content [] = [
{
kind: 'text' ,
text: log . host ,
variant: 'caption' ,
},
];
if ( log . response ) {
children . push ({
kind: 'text' ,
text: ` ${ log . response . duration } ms` ,
variant: 'caption' ,
});
}
return {
key: 'footer' ,
kind: 'box' ,
direction: 'row' ,
justifyContent: 'space-between' ,
children ,
};
}
provideDetailContent ( log : NetworkingLog ) : DetailContent {
return {
kind: 'tab-bar' ,
tabs: [
{
title: 'Info' ,
content: this . provideInfoContent ( log ),
},
{
title: 'Request' ,
content: this . provideRequestContent ( log . request ),
},
{
title: 'Response' ,
content: this . provideResponseContent ( log . response ),
},
],
};
}
// ... additional helper methods for tabs ...
}
Key features:
Tabbed interface (Info, Request, Response)
Shows host and duration in footer
Renders headers and body for both request and response
JSON formatting for request/response bodies
Handles loading state for pending requests
The NetworkingLogPlugin demonstrates advanced content types like tabs, sections, and JSON viewers. Use it as a reference when building complex custom plugins.
Plugin Registration with Beagle.registerPlugin()
Plugins must be registered before their log types are used. Registration typically happens in the provider:
useEffect (() => {
Beagle . registerPlugin ( new MessageLogPlugin ());
Beagle . registerPlugin ( new ErrorLogPlugin ());
}, []);
Plugins should be registered in a useEffect to ensure they’re only registered once, even if the component re-renders.
Registration Order
Plugins are checked in registration order. If multiple plugins can handle the same log type, the first registered plugin wins.
Best practice: Register more specific plugins before generic ones.
Conditional Registration
You can conditionally register plugins based on configuration:
useEffect (() => {
Beagle . registerPlugin ( new MessageLogPlugin ());
Beagle . registerPlugin ( new ErrorLogPlugin ());
if ( enableNetworkLogging ) {
Beagle . registerPlugin ( new NetworkingLogPlugin ());
}
}, [ enableNetworkLogging ]);
Creating Custom Plugins
To create a custom plugin:
Create a custom log class extending BeagleLog
Create a plugin class extending BeagleLogPlugin<YourLog>
Implement required methods (name, canHandle, provideDetailContent)
Optionally override provideCardFooter and exportToJSON
Register the plugin during app initialization
Example:
// 1. Custom log class
export class AnalyticsLog extends BeagleLog {
eventName : string ;
properties : Record < string , any >;
constructor ( eventName : string , properties : Record < string , any >) {
super ( `Analytics: ${ eventName } ` , 'info' );
this . eventName = eventName ;
this . properties = properties ;
}
}
// 2. Custom plugin
export class AnalyticsLogPlugin extends BeagleLogPlugin < AnalyticsLog > {
name = 'Analytics' ;
canHandle ( log : BeagleLog ) : log is AnalyticsLog {
return log instanceof AnalyticsLog ;
}
provideDetailContent ( log : AnalyticsLog ) : DetailContent {
return {
kind: 'list' ,
children: [
{ kind: 'label' , label: 'Event' , value: log . eventName },
{ kind: 'json' , data: log . properties },
],
};
}
provideCardFooter ( log : AnalyticsLog ) : Content {
return {
kind: 'text' ,
text: ` ${ Object . keys ( log . properties ). length } properties` ,
variant: 'caption' ,
};
}
}
// 3. Register
Beagle . registerPlugin ( new AnalyticsLogPlugin ());
// 4. Use
const log = new AnalyticsLog ( 'page_view' , { page: '/home' , user_id: '123' });
BeaglePlugins . log ( log );
This plugin system makes Beagle infinitely extensible while maintaining a consistent user experience.