Pharos Triton
  • 🔱About Pharos Triton
  • 🏁Installing Pharos Triton
  • Apex Logging Basics
  • Common Apex Usage Patters
    • Batch Logging
    • Integration Logs
    • Apex Rest Logging
    • Full Control with TritonBuilder
  • Beyond Apex
    • LWC
    • 🔄LWC Transaction Management
    • ⚡LWC and Apex
    • 💾Platform Cache for Transactions
    • Flows
    • 〰️LWC, Apex and Flows
  • 📖Methods Reference
    • 📔Apex
      • Triton
      • TritonBuilder
      • TritonTypes
      • TritonLwc
        • ComponentLog
        • Component
        • Error
        • RuntimeInfo
      • TritonFlow
        • FlowLog
      • TritonHelper
        • PostProcessingControlsBuilder
      • LogBuilder
    • LWC
      • Triton
      • TritonBuilder
      • TritonUtils
  • Help and Support
Powered by GitBook
On this page
  • Component Binding
  • Creating an LWC Logger
  • Components Without @wire
  • Components With @wire
  • Inside Triton.js
  • TritonBuilder Pattern
  • Component-Specific Templates
  • Setting Up Templates
  • Using Templates
  • Buffered vs Immediate Logging
  • Real-World Logging Scenario
  • Advanced Wire Patterns with Logging
  1. Beyond Apex

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:

  1. Component Identification: Each log entry is automatically tagged with the component that generated it

  2. Template Management: Component-specific log templates are properly scoped and managed

  3. 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:

logDemo.js
//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:

  1. Constructor runs

  2. @wire decorators are initialized and begin fetching data

  3. connectedCallback executes

  4. 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 ...
    }
}

Best Practice: While we've shown both initialization patterns, it's actually perfectly safe and arguably cleaner to always initialize Triton in the constructor, regardless of whether your component uses @wire or not. The constructor initialization will work in all scenarios because:

  1. It ensures the logger is available as early as possible in the component lifecycle

  2. Triton uses the singleton pattern, so there's no performance penalty

  3. It reduces cognitive overhead - you don't need to think about where to initialize based on @wire usage

  4. It future-proofs your component if you later add @wire decorators

The only minor consideration is that if your component is never actually connected to the DOM (very rare), you'll have initialized Triton unnecessarily. However, this edge case rarely outweighs the benefits of consistent initialization.

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')
        );
    }
}

Templates are automatically scoped to the component that created them thanks to Triton's proxy-based component binding. This means:

  • Each component can have its own template

  • Templates won't interfere with each other

  • Template methods (setTemplate and fromTemplate) are only available after calling bindToComponent()

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.

When using logNow(), only the specific log entry passed to the method is sent to the server immediately. Any other logs in the buffer remain there until the next flush operation. This selective immediate logging is particularly useful when you need to ensure critical logs are saved without affecting the buffering of less urgent logs.

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:

    1. They need to be available right away for troubleshooting

    2. The error might prevent subsequent operations from completing

    3. Support teams can respond in real-time to mission-critical failures

    4. The error context is preserved even if the component crashes

    5. 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:

  1. Programmatic Refresh: The wiredOrderItems property provides a reference that can be used to programmatically refresh the data using refreshApex(this.wiredOrderItems).

  2. 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

The dual-wire pattern doesn't cause double network requests - LWC's wire service is smart enough to share a single subscription for identical wire configurations.

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.

PreviousBeyond ApexNextLWC Transaction Management

Last updated 1 month ago