LWC
In this article we'll take a look at logging in LWC components and familiarize ourselves with Triton's JS interface.
LWC is a powerful UI framework in use today by organizations of all sizes. LWC provides a wealth of capabilities that extend the platform's user interface and allow developers to create rich visual experiences. However, with great power comes complexity.
It's quite common for LWC-based interfaces to become large and complex. At that point, one needs a little something to help manage the expanding functionality without loosing sanity. As you may have guessed, this is yet another area to leverage Pharos Triton. Let's first examine the basics.
Component Binding
When initializing Triton in an LWC component, it's crucial to use the bindToComponent()
method. This method creates a component-specific context for logging that enables:
Component Identification: Each log entry is automatically tagged with the component that generated it
Template Management: Component-specific log templates are properly scoped and managed
Stack Trace Accuracy: Error traces and component details are correctly captured
Here's the proper way to bind Triton to your component:
this.tritonLogger = new Triton().bindToComponent('myComponentName');
The bindToComponent()
argument should be a unique identifier for your component, typically matching the component's name. This binding ensures that when debugging issues in production, you can easily trace logs back to their source component.
Always use bindToComponent()
when initializing Triton. Without it, you'll lose important component context in your logs and template functionality won't work correctly.
Creating an LWC Logger
The good news is that getting a hold of the Triton logger in LWC is quite easy. The logger component is called triton. To access its capabilities, simply include it in your controller class. The initialization timing depends on whether your component uses @wire methods:
Components Without @wire
For components without @wire decorators, initialize Triton in the connectedCallback() method. This is safe because connectedCallback is one of the first lifecycle hooks that fires when a component is added to the DOM, and without @wire decorators, there's no risk of missing early data or error events:
//Triton logger component
import Triton, { TYPE, AREA, LEVEL } from 'c/triton';
export default class LogDemo extends LightningElement {
//instance of the logger
tritonLogger;
connectedCallback() {
//create new logger - uses singleton pattern
this.tritonLogger = new Triton().bindToComponent('logDemo');
}
//let's utilize our logger
async handleClick(event) {
....
//invoke some action
someAction({}).then(() => {
// Buffer the log for later sending
this.tritonLogger.log(
this.tritonLogger.info(TYPE.FRONTEND, AREA.ACCOUNTS)
.summary('Action completed successfully')
.details(JSON.stringify({ someKey: 'value' }))
);
}).catch(async (error) => {
// Immediately send the error log
await this.tritonLogger.logNow(
this.tritonLogger.exception(error)
.summary('Action failed')
.details(JSON.stringify({ eventDetails: event }))
);
});
....
}
}
Components With @wire
When using @wire decorators, we need to be more careful about initialization timing. The Lightning Web Component lifecycle follows this sequence:
Constructor runs
@wire decorators are initialized and begin fetching data
connectedCallback executes
Component renders
This means that if we initialize Triton in connectedCallback, we might miss important wire-related events that occur between steps 2 and 3. Therefore, for components with @wire decorators, we initialize in the constructor.
export default class LogDemoWithWire extends LightningElement {
tritonLogger;
constructor() {
super();
this.tritonLogger = new Triton().bindToComponent('logDemoWithWire');
}
@wire(getRecord, { /* wire parameters */ })
wiredRecord({ error, data }) {
if (error) {
this.tritonLogger.logNow(
this.tritonLogger.exception(error)
.summary('Failed to load record')
);
}
// ... handle data ...
}
}
Inside Triton.js
The LWC version of Triton implements a robust logging system with transaction management and automatic log flushing. The logger provides different logging levels (error, warning, info, debug) and uses a builder pattern for constructing logs with rich context.
Each log can include details such as:
Transaction ID for tracking related logs
User ID and timestamp
Component stack traces
Runtime information (browser, device, performance metrics)
Summary and detailed information
The logger automatically manages log buffering and flushing to the server, with an auto-flush mechanism that triggers after a period of inactivity.
TritonBuilder Pattern
Triton uses a builder pattern that allows for fluent, chainable logging statements. Each logging method (error, warning, info, debug) returns a builder instance that you can use to add additional context to your log:
this.tritonLogger.log(
this.tritonLogger.error(TYPE.FRONTEND, AREA.ACCOUNTS)
.summary('Failed to load account details')
.details(JSON.stringify({ accountId: '123' }))
.relatedObjects(['001xx000003DGb2AAG'])
);
The builder captures rich contextual information automatically, including:
Component details and stack traces
Runtime information (browser, device, etc.)
Transaction IDs
Timestamps
User context
Each builder method is chainable, allowing you to construct logs with as much or as little detail as needed:
const startTime = performance.now();
// ... perform operation ...
this.tritonLogger.log(
this.tritonLogger.debug(TYPE.FRONTEND, AREA.ACCOUNTS)
.summary('Processing account update')
.details(JSON.stringify(accountData))
.duration(performance.now() - startTime)
.relatedObjects([accountId])
);
Component-Specific Templates
Triton provides a powerful templating system that allows you to define logging templates specific to each component. When you use bindToComponent()
, Triton ensures that templates are properly scoped to your component through method proxying.
Setting Up Templates
You can define templates with common properties that you'll reuse across your component:
export default class AccountManager extends LightningElement {
tritonLogger;
connectedCallback() {
this.tritonLogger = new Triton().bindToComponent('AccountManager');
// Set up a component-specific template
this.tritonLogger.setTemplate(
this.tritonLogger.makeBuilder()
.type(TYPE.FRONTEND)
.area(AREA.ACCOUNTS)
.level(LEVEL.INFO)
.relatedObjects([this.recordId])
);
}
}
Using Templates
Once defined, you can use the template as a base for your logs, adding only the specific details that change:
async handleAccountUpdate() {
try {
await updateAccount(this.accountId);
// Uses the template and adds specific details
this.tritonLogger.log(
this.tritonLogger.fromTemplate()
.summary('Account updated successfully')
.details(JSON.stringify({ accountId: this.accountId }))
);
} catch (error) {
// Template properties are preserved in error logs too
this.tritonLogger.logNow(
this.tritonLogger.fromTemplate()
.exception(error)
.summary('Failed to update account')
);
}
}
This templating system is particularly valuable when:
Your component generates many similar logs
You want to ensure consistent logging patterns
You need to maintain common context across all component logs
You want to reduce repetitive logging code
Buffered vs Immediate Logging
Triton provides two methods for sending logs to the server:
log()
: Buffers the log entry for later sending. Multiple logs will be sent together during periodic flush operations or when explicitly flushed. This is more efficient for high-volume logging.logNow()
: Immediately sends the log entry to the server. This is useful for critical errors or when immediate logging feedback is required. Returns a Promise that resolves when the log is saved.
For example:
// This log will be buffered
this.triton.log(
this.triton.debug(TYPE.FRONTEND, AREA.ACCOUNTS)
.summary('Debug info')
);
// This error log will be sent immediately
await this.triton.logNow(
this.triton.error(TYPE.FRONTEND, AREA.ACCOUNTS)
.summary('Critical error')
);
// Previously buffered debug log is still in the buffer
Real-World Logging Scenario
Consider a complex account management interface where users can perform multiple operations:
async handleAccountOperations() {
// Buffer debug logs for routine operations
this.triton.log(
this.triton.debug(TYPE.FRONTEND, AREA.ACCOUNTS)
.summary('Starting account update sequence')
);
try {
// Perform multiple account operations
await this.updateAccountDetails();
// Buffer success logs
this.triton.log(
this.triton.info(TYPE.FRONTEND, AREA.ACCOUNTS)
.summary('Account details updated successfully')
.details(JSON.stringify({ accountId: this.accountId }))
);
} catch (error) {
// Immediately log critical errors
await this.triton.logNow(
this.triton.exception(error)
.summary('Failed to update account')
.details(JSON.stringify({
accountId: this.accountId,
operation: 'updateAccountDetails',
errorDetails: error.message
}))
);
// Show error to user with support notification
this.showErrorMessage(
'Failed to update account. A support team member has been notified and will contact you shortly.'
);
}
}
In this scenario:
Debug and success logs are buffered because they're routine and non-critical
Error logs are sent immediately because:
They need to be available right away for troubleshooting
The error might prevent subsequent operations from completing
Support teams can respond in real-time to mission-critical failures
The error context is preserved even if the component crashes
Time-sensitive operations can trigger immediate support intervention
This immediate logging pattern is particularly valuable in scenarios like financial transactions, healthcare operations, or other mission-critical processes where real-time monitoring and rapid response are essential.
Advanced Wire Patterns with Logging
When working with wired properties in LWC, there's a powerful pattern that combines programmatic refresh capabilities with logging. This involves wiring the same Apex method to two different properties:
export default class OrderManager extends LightningElement {
@wire(getOrderItems, { orderId: '$recordId' })
wiredOrderItems;
@wire(getOrderItems, { orderId: '$recordId' })
logOrderItemsChange({ error, data }) {
if (data) {
this.setOrderItems(data);
this.triton.log(
this.triton.fromTemplate()
.level(LEVEL.DEBUG)
.summary('Order items retrieved successfully')
.relatedObjects([this.recordId])
.details(`Retrieved ${data.length} order items`)
);
} else if (error) {
this.error = error;
this.triton.log(
this.triton.fromTemplate()
.level(LEVEL.ERROR)
.summary('Error retrieving order items')
.relatedObjects([this.recordId])
.exception(error)
);
}
}
}
This pattern serves two important purposes:
Programmatic Refresh: The
wiredOrderItems
property provides a reference that can be used to programmatically refresh the data usingrefreshApex(this.wiredOrderItems)
.Automatic Logging: The second wire decorator with the callback function handles data processing and automatically logs both successful retrievals and errors.
This approach is particularly useful when you need to:
Track all data loading attempts, both automatic and manual refreshes
Maintain a detailed audit trail of data operations
Debug intermittent loading issues
Monitor performance patterns in data retrieval
Now that we've covered some basics, let's have a look at a more interesting example and combine the LWC logger with its Apex counterpart.
Last updated