diff --git a/README.md b/README.md index 0868b10bf..861e6744e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build](https://github.com/jongpie/NebulaLogger/actions/workflows/build.yml/badge.svg)](https://github.com/jongpie/NebulaLogger/actions/workflows/build.yml) [![codecov](https://codecov.io/gh/jongpie/NebulaLogger/branch/main/graph/badge.svg?token=1DJPDRM3N4)](https://codecov.io/gh/jongpie/NebulaLogger) +[![skills.sh](https://skills.sh/b/jongpie/NebulaLogger)](https://skills.sh/jongpie/NebulaLogger) The most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, OmniStudio, and integrations. @@ -25,6 +26,16 @@ The most robust observability solution for Salesforce experts. Built 100% native --- +## AI Agent Skills + +Nebula Logger includes [Agent Skills](https://www.skills.sh/docs) that help AI coding agents (Claude Code, GitHub Copilot, Cursor, and others) install, configure, and use Nebula Logger with the recommended patterns. + +```bash +npx skills add jongpie/NebulaLogger +``` + +--- + ## Features 1. A unified logging tool that supports easily adding log entries across the Salesforce platform, using: diff --git a/nebula-logger/extra-tests/integration-tests/classes/LogEntryEventBuilder_Tests_Network.cls b/nebula-logger/extra-tests/integration-tests/classes/LogEntryEventBuilder_Tests_Network.cls index b97a26373..33696be7a 100644 --- a/nebula-logger/extra-tests/integration-tests/classes/LogEntryEventBuilder_Tests_Network.cls +++ b/nebula-logger/extra-tests/integration-tests/classes/LogEntryEventBuilder_Tests_Network.cls @@ -72,13 +72,17 @@ private class LogEntryEventBuilder_Tests_Network { } static Schema.User setupExperienceSiteUser() { - Schema.UserRole userRole = new Schema.UserRole(DeveloperName = 'LoggerTestRole', Name = 'Logger Test Role'); + Schema.UserRole userRole = new Schema.UserRole( + DeveloperName = 'LoggerTestRole_' + System.UUID.randomUUID().toString().replace('-', '_'), + Name = 'Logger Test Role' + ); insert userRole; - Schema.User currentUser = new Schema.User(Id = System.UserInfo.getUserId(), UserRoleId = userRole.Id); - update currentUser; + Schema.User accountOwnerUser = LoggerMockDataCreator.createUser(); + accountOwnerUser.UserRoleId = userRole.Id; + Schema.User experienceSiteUser; - System.runAs(currentUser) { - Schema.Account account = new Schema.Account(Name = 'Test Account', OwnerId = currentUser.Id); + System.runAs(accountOwnerUser) { + Schema.Account account = new Schema.Account(Name = 'Test Account', OwnerId = accountOwnerUser.Id); insert account; Contact contact = new Contact(AccountId = account.Id, LastName = 'testcontact'); insert contact; @@ -87,6 +91,7 @@ private class LogEntryEventBuilder_Tests_Network { experienceSiteUser.ContactId = contact.Id; insert experienceSiteUser; } + return [SELECT Id, AccountId, ContactId FROM User WHERE Id = :experienceSiteUser.Id]; } } diff --git a/nebula-logger/extra-tests/integration-tests/triggers/User_Logger_Mixed_DML_Example.trigger b/nebula-logger/extra-tests/integration-tests/triggers/User_Logger_Mixed_DML_Example.trigger index ad6727894..93ed8536b 100644 --- a/nebula-logger/extra-tests/integration-tests/triggers/User_Logger_Mixed_DML_Example.trigger +++ b/nebula-logger/extra-tests/integration-tests/triggers/User_Logger_Mixed_DML_Example.trigger @@ -24,7 +24,11 @@ trigger User_Logger_Mixed_DML_Example on User(after insert) { Id targetGroupId = matchingGroups.get(0).Id; List newGroupMembers = new List(); for (Schema.User insertedUser : Trigger.new) { - newGroupMembers.add(new Schema.GroupMember(GroupId = targetGroupId, UserOrGroupId = insertedUser.Id)); + // Experience Cloud users can't be added to the public group (and doing so isn't relevant for the tests), + // so skip any Experience Cloud users. + if (insertedUser.ContactId == null) { + newGroupMembers.add(new Schema.GroupMember(GroupId = targetGroupId, UserOrGroupId = insertedUser.Id)); + } } insert newGroupMembers; } diff --git a/nebula-logger/managed-package/sfdx-project.json b/nebula-logger/managed-package/sfdx-project.json index 61b120052..244582888 100644 --- a/nebula-logger/managed-package/sfdx-project.json +++ b/nebula-logger/managed-package/sfdx-project.json @@ -37,6 +37,6 @@ "Nebula Logger - Managed Package@4.16.0-spring-25-release": "04t5Y0000015pGtQAI", "Nebula Logger - Managed Package@4.17.0-summer-25-release": "04tg70000000r5xAAA", "Nebula Logger - Managed Package@4.18.0-winter-26-release": "04tg700000086RdAAI", - "Nebula Logger - Managed Package@4.19.0-spring-26-release": "04tg700000086RdAAI" + "Nebula Logger - Managed Package@4.19.0-spring-26-release": "04tg7000000GZbJAAW" } } diff --git a/scripts/build/create-and-install-package-version.ps1 b/scripts/build/create-and-install-package-version.ps1 index 597735f75..d26554c31 100644 --- a/scripts/build/create-and-install-package-version.ps1 +++ b/scripts/build/create-and-install-package-version.ps1 @@ -1,5 +1,5 @@ # This script is used to create a new package version for the specified package alias -# It then auto-adds the new package version the files sfdx-project.json and README.md +# It then auto-adds the new package version the files sfdx-project.json, README.md, and the install skill # Finally, the new package version is installed it into an org, using the specified target username param ([string]$targetpackagealias, [string]$targetreadme, [string]$targetusername) @@ -124,6 +124,29 @@ function Update-README-Package-Version-Id { ((Get-Content -path $targetreadme -Raw) -replace "sf package install --wait 20 --security-type AdminsOnly --package .{0,18}", $sfUnlockedPackageReplacement) | Set-Content -Path $targetreadme -NoNewline } +function Update-Install-Skill-Package-Version-Id { + param ( + $packageVersionId + ) + + $installSkillPath = "./skills/nebula-logger-install/SKILL.md" + if (-not (Test-Path $installSkillPath)) { + Write-Debug "Install skill not found at $installSkillPath, skipping" + return + } + + $packageVersionId = "$packageVersionId".Trim() + # Only the unlocked package IDs are refreshed here - the managed package IDs are updated manually on managed releases. + # The regex only matches the unlocked URLs because the managed URLs include "mgd=true&" between the query string and "p0=" + $sandboxUnlockedReplacement = "Sandbox install link: ``https://test.salesforce.com/packaging/installPackage.apexp?p0=$packageVersionId``" + ((Get-Content -path $installSkillPath -Raw) -replace "Sandbox install link: ``https:\/\/test.salesforce.com\/packaging\/installPackage.apexp\?p0=.{0,18}``", $sandboxUnlockedReplacement) | Set-Content -Path $installSkillPath -NoNewline + $productionUnlockedReplacement = "Production install link: ``https://login.salesforce.com/packaging/installPackage.apexp?p0=$packageVersionId``" + ((Get-Content -path $installSkillPath -Raw) -replace "Production install link: ``https:\/\/login.salesforce.com\/packaging\/installPackage.apexp\?p0=.{0,18}``", $productionUnlockedReplacement) | Set-Content -Path $installSkillPath -NoNewline + # The `--wait 20` value disambiguates the unlocked CLI command from the managed one (which uses `--wait 30`) + $sfUnlockedReplacement = "Salesforce CLI: ``sf package install --wait 20 --security-type AdminsOnly --package $packageVersionId``" + ((Get-Content -path $installSkillPath -Raw) -replace "Salesforce CLI: ``sf package install --wait 20 --security-type AdminsOnly --package .{0,18}``", $sfUnlockedReplacement) | Set-Content -Path $installSkillPath -NoNewline +} + function Install-Package-Version { param ( $packageVersionId @@ -156,5 +179,13 @@ Update-README-Package-Version-Id $packageVersionId npx prettier --write $targetreadme git add $targetreadme +$installSkillPath = "./skills/nebula-logger-install/SKILL.md" +if (Test-Path $installSkillPath) { + Write-Debug "Adding new package version ID $packageVersionId to $installSkillPath" + Update-Install-Skill-Package-Version-Id $packageVersionId + npx prettier --write $installSkillPath + git add $installSkillPath +} + Write-Debug "Installing new package version ID $packageVersionId for target user $targetusername" Install-Package-Version $packageVersionId diff --git a/scripts/build/sync-package-version-number.ps1 b/scripts/build/sync-package-version-number.ps1 index 0c3c79540..6e5f1b3a6 100644 --- a/scripts/build/sync-package-version-number.ps1 +++ b/scripts/build/sync-package-version-number.ps1 @@ -4,6 +4,7 @@ $sfdxProjectJsonPath = "./sfdx-project.json" $packageJsonPath = "./package.json" $readmeClassPath = "./README.md" +$installSkillPath = "./skills/nebula-logger-install/SKILL.md" $loggerClassPath = "./nebula-logger/core/main/logger-engine/classes/Logger.cls" $loggerComponentPath = "./nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js" @@ -54,6 +55,29 @@ function Update-README { git add $readmeClassPath } +function Get-Install-Skill { + Get-Content -Raw -Path $installSkillPath +} + +function Update-Install-Skill { + param ( + $versionNumber + ) + if (-not (Test-Path $installSkillPath)) { + Write-Output "Install skill not found at $installSkillPath, skipping" + return + } + $versionNumber = "v" + $versionNumber + $installSkillContents = Get-Install-Skill + Write-Output "Bumping install skill unlocked package version number to: $versionNumber" + + $targetRegEx = "(### Unlocked Package - )(v[\d\.]+)" + $replacementRegEx = '$1' + $versionNumber + $installSkillContents -replace $targetRegEx, $replacementRegEx | Set-Content -Path $installSkillPath -NoNewline + npx prettier --write $installSkillPath + git add $installSkillPath +} + function Get-Logger-Class { Get-Content -Raw -Path $loggerClassPath } @@ -97,5 +121,6 @@ Write-Output "Target Version Number: $versionNumber" Update-Package-JSON $versionNumber Update-README $versionNumber +Update-Install-Skill $versionNumber Update-Logger-Class $versionNumber Update-Logger-Component $versionNumber diff --git a/skills.sh.json b/skills.sh.json new file mode 100644 index 000000000..12204ef8d --- /dev/null +++ b/skills.sh.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://skills.sh/schemas/skills.sh.schema.json", + "notGrouped": "bottom", + "groupings": [ + { + "title": "Setup", + "description": "Installation and configuration of Nebula Logger in a Salesforce org.", + "skills": ["nebula-logger-install"] + }, + { + "title": "Usage", + "description": "Writing logging code across Apex, LWC, Aura, Flow, and OmniStudio, and testing code that uses Nebula Logger.", + "skills": ["nebula-logger-instrumentation", "nebula-logger-testing-your-code"] + }, + { + "title": "Operations", + "description": "Day-to-day console navigation and log retention / purging administration.", + "skills": ["nebula-logger-console", "nebula-logger-purging-and-retention"] + }, + { + "title": "Governance", + "description": "Best practices, logging levels, and governor limit considerations.", + "skills": ["nebula-logger-best-practices"] + }, + { + "title": "Extending", + "description": "Building custom plugins that extend Nebula Logger.", + "skills": ["nebula-logger-plugin-development"] + } + ] +} diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 000000000..822a88538 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,29 @@ +# Nebula Logger Skills + +This folder contains Nebula Logger's [skills.sh](https://skills.sh) catalog. Each subfolder is one skill, with a `SKILL.md` file that describes when the skill applies and what it covers. The top-level [`skills.sh.json`](../skills.sh.json) groups the skills into logical categories for the catalog page at [skills.sh/jongpie/NebulaLogger](https://skills.sh/jongpie/NebulaLogger). + +## Skills + +| Skill | Purpose | +| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [nebula-logger-install](./nebula-logger-install/SKILL.md) | Installing Nebula Logger, choosing between unlocked and managed packages, permission-set assignment, and initial `LoggerSettings__c` configuration. | +| [nebula-logger-instrumentation](./nebula-logger-instrumentation/SKILL.md) | Adding Nebula Logger instrumentation in Apex, LWC, Aura, Flow, and OmniStudio. Covers logging APIs, exception handling, record association, tags/scenarios, async transaction linking, and the `CallableLogger` optional-dependency pattern for ISVs. | +| [nebula-logger-console](./nebula-logger-console/SKILL.md) | Browsing and investigating logs in the Salesforce UI - list views, record pages, home page components, and the live log entry event stream. | +| [nebula-logger-purging-and-retention](./nebula-logger-purging-and-retention/SKILL.md) | Configuring how long logs live before being purged - `LogRetentionDate__c` semantics, `LogBatchPurger` scheduling, and per-user/profile/scenario overrides. | +| [nebula-logger-testing-your-code](./nebula-logger-testing-your-code/SKILL.md) | Writing Apex and LWC tests for code that calls Nebula Logger. Global-only APIs and patterns for observing entries without polluting the org. | +| [nebula-logger-plugin-development](./nebula-logger-plugin-development/SKILL.md) | Building custom plugins that extend Nebula Logger via the `LoggerPlugin.Triggerable` and `LoggerPlugin.Batchable` interfaces. | +| [nebula-logger-best-practices](./nebula-logger-best-practices/SKILL.md) | Team-wide instrumentation standards, environment-aware logging levels, and governor-limit considerations. | + +## Supported API Surface + +Every skill in this catalog references only Nebula Logger's `global` Apex surface and the exported `c/logger` LWC module. `public` classes and methods in the unlocked package are technically reachable but are Nebula Logger's internal surface and can change without a deprecation window - see the "Supported API Surface" section in [nebula-logger-instrumentation](./nebula-logger-instrumentation/SKILL.md) for the full policy. + +The plugin framework in [nebula-logger-plugin-development](./nebula-logger-plugin-development/SKILL.md) is a deliberate exception: the extension points are `public` because they exist only inside the unlocked package, and plugin authors interact with them by design. Plugins should pin to a tested Nebula Logger version and re-verify on upgrades. + +## Publishing + +skills.sh reads this repository directly from GitHub. Landing a change on `main` publishes it - no separate submission or manual crawl trigger is needed. + +## Contributing + +If you spot something in a skill that no longer matches the code (a renamed field, a removed method, a stale example), please file an issue at https://github.com/jongpie/NebulaLogger/issues or open a PR. See the repo's [CONTRIBUTING.md](../CONTRIBUTING.md) for the general contribution workflow. diff --git a/skills/nebula-logger-best-practices/SKILL.md b/skills/nebula-logger-best-practices/SKILL.md new file mode 100644 index 000000000..13b5dfe1e --- /dev/null +++ b/skills/nebula-logger-best-practices/SKILL.md @@ -0,0 +1,78 @@ +--- +name: nebula-logger-best-practices +description: Use this skill when the user wants to review, harden, or standardize Nebula Logger usage across a Salesforce team. Covers operational logging standards, environment-aware settings, limit-aware design, and governance guardrails. +--- + +# Nebula Logger Best Practices and Governance + +## Team-Wide Practices + +Use these defaults when reviewing pull requests or designing logging conventions. + +1. Reserve `ERROR`, `WARN`, and `INFO` for information that is operationally significant. +2. Use `DEBUG`, `FINE`, `FINER`, and `FINEST` for high-volume diagnostic detail. Combined with `LoggerSettings__c.LoggingLevel__c`, these can be left in code without adding runtime overhead in production and switched on only when deeper diagnostics are needed. +3. Tune `LoggerSettings__c` data by environment. + - Configure `LoggingLevel__c` to `ERROR`, `WARN`, or `INFO` in production orgs to reduce logging noise (or change to `DEBUG`, `FINE`, `FINER`, or `FINEST` when trying to debug) + - Configure scheduled purging and retention windows (`DefaultLogPurgeAction__c`, `DefaultNumberOfDaysToRetainLogs__c`) +4. Use static method `Logger.setScenario()`, and instance methods `LogEntryEventBuilder.addTag()` and `LogEntryEventBuilder.addTags()` for business-process grouping. + - Define a controlled naming convention for scenarios & tags that make sense to your team +5. Use instance methods on `LogEntryEventBuilder` to further enrich data, instead of embedding extra data directly in message strings. There are several method overloads available: + - `.setExceptionDetails(...)` + - `.setApprovalResult(...)` + - `.setDatabaseResult(...)` + - `.setRecord(...)` + - `.setHttpRequestDetails(...)` + - `.setHttpResponseDetails(...)` + - `.setRestRequestDetails(...)` + - `.setRestResponseDetails(...)` + - `.setField(...)` +6. Call `Logger.saveLog()` deliberately - don't call it after every log entry, and never inside a tight loop. Be strategic when calling it, just like when making DML calls in Apex. + - `Logger.info(...)` / `.error(...)` / etc. only add entries to an in-memory buffer. Nothing persists until `saveLog()` runs. If a transaction ends without calling `saveLog()`, everything that was buffered is lost - so `saveLog()` still has to be called before the transaction commits. + - Every `saveLog()` call is a real platform operation with real cost. With the default `EVENT_BUS` save method, each call to `saveLog()` calls `System.EventBus.publish(List)` once, which consumes one slot against `System.Limits.getLimitPublishImmediateDML()` (100 per transaction) and one increment against the org's daily platform event publish allocation. The other save methods have their own limits: `QUEUEABLE` consumes an async job slot (`System.Limits.getLimitQueueableJobs()`), `REST` consumes a callout, and `SYNCHRONOUS_DML` consumes regular DML rows and statements. None of them are free. + - Multiple `saveLog()` calls in a transaction are fine, and often the right choice. Reasonable places to save intermediate state include after each chunk in a batch, at the end of each iteration of a long-running loop's outer scope (never the inner scope - see below), before an async handoff (Queueable / Future / Batchable), and inside a `finally` block that catches an exception you're about to rethrow. Each save publishes only what's in the buffer at that moment, so the entries persist even if the rest of the transaction later blows up. + - What's wrong is calling `saveLog()` after every single log entry. That turns N log calls into N platform-event publishes, burns through `getLimitPublishImmediateDML()` (100 per transaction) fast, and eats into the org's daily platform event allocation for no operational benefit. If you find yourself typing `Logger.info(...); Logger.saveLog();` repeatedly, buffer the entries and save once after the group. + - Never call `saveLog()` inside the innermost body of a loop over records. Buffer entries in the loop and call `saveLog()` after the loop (or at safe checkpoints - after N iterations, after a chunk of work, etc.), not after each record. + - Place a `Logger.saveLog()` call in a `finally` block for transactional code paths so it still runs when an exception escapes the try. Pair it with `Logger.setSaveMethod(...)` at the top of the method if the default `EVENT_BUS` isn't right for that path (see the save-method notes above). + +## Environment-Aware Logging Levels + +Treat logging verbosity as an operational control, not as a code constant. + +| Environment | Suggested baseline | Reason | +| ----------- | ------------------------------- | ---------------------------------------------------------------- | +| Production | `ERROR` or `WARN` | Reduce noise and storage impact while preserving incident signal | +| UAT | `INFO` | Validate business flows without excessive detail | +| QA | `DEBUG` or `FINE` | Support defect reproduction and integration testing | +| Sandbox/Dev | `FINE` to `FINEST` (time-boxed) | Deep diagnostics during active development | + +Use hierarchy overrides (user/profile) for temporary incident windows, then revert. + +## Review Checklist for Existing Code + +- Does each execution path call `saveLog()` at deliberate checkpoints - and never after every entry or inside a per-record inner loop? +- Are exceptions logged with stack trace context? +- Are record references attached via `.setRecord(...)`? +- Are scenarios and tags consistent with team taxonomy? +- Is save method choice explicit when not using default `EVENT_BUS`? + +## Governor and Capacity Considerations + +Nebula Logger helps observability, but it still runs inside Salesforce governor boundaries. + +| Constraint | Impact on logging | Mitigation | +| -------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| Data storage | High-volume logs can consume custom object storage quickly | Enforce retention policy and purge schedule; lower verbosity in production | +| Platform event daily limit | `EVENT_BUS` volume can hit org event allocations | Use selective logging and switch targeted jobs to `QUEUEABLE`/`SYNCHRONOUS_DML` when needed | +| SOQL queries | Extra enrichment queries in logging paths can compound limits | Reuse queried records and avoid logging-only query fanout | +| CPU time | Heavy serialization/tagging in loops increases CPU usage | Log aggregate milestones, not every iteration, and prefer async save paths | +| Async job allocation | Overusing `QUEUEABLE` can compete with business async work | Reserve queueable saves for high-cost or mixed-DML-sensitive contexts | +| Heap size | Large payloads/exceptions can inflate transaction memory | Truncate oversized payloads and avoid dumping full object graphs | + +## When Not to Use Nebula Logger + +Nebula Logger is not a substitute for sound architecture. + +- Do not use logging as a workaround for unclear domain design. +- Do not store secrets, credentials, or unnecessary PII in log messages. + +Use Nebula Logger as a structured observability layer, combined with clean service boundaries, resilient error handling, and purposeful telemetry. diff --git a/skills/nebula-logger-console/SKILL.md b/skills/nebula-logger-console/SKILL.md new file mode 100644 index 000000000..a1c069a58 --- /dev/null +++ b/skills/nebula-logger-console/SKILL.md @@ -0,0 +1,99 @@ +--- +name: nebula-logger-console +description: Use this skill when the user wants to browse, filter, or investigate Nebula Logger data in the Salesforce UI. Covers the Logger Console app, list views on `Log__c` / `LogEntry__c` / `LogEntryTag__c` / `LoggerScenario__c` / `LoggerTag__c`, related-list navigation, the Log record page and its LWCs, and how admins and support engineers use the console day-to-day. +--- + +# Using the Nebula Logger Console + +## What the Console Is + +The Logger Console is a Salesforce Lightning App shipped with Nebula Logger. It gives admins, support engineers, and developers a purpose-built place to browse logs without hand-writing SOQL against the log-management custom objects (`Log__c`, `LogEntry__c`, `LogEntryTag__c`, `LoggerScenario__c`, `LoggerTag__c`) every time something happens in production. + +- App API name: `LoggerConsole`. +- Navigation entries include `Log__c`, `LogEntry__c`, `LoggerScenario__c`, `LoggerTag__c`, and `LogEntryTag__c`. +- A utility bar (`LoggerConsoleUtilityBar` FlexiPage) surfaces a `Logger Settings` panel for editing the effective hierarchy record without leaving the console. + +## Standard List Views + +Nebula Logger ships a broad set of list views across all five log-management custom objects so most day-one investigations don't require a custom view: + +- **`Log__c`**: `AllLogs`, `AllOpenLogs`, `AllClosedLogs`, `AllResolvedLogs`, `AllLogsWithERROREntries`, `AllLogsWithERRORorWARNEntries`, `AllAsynchronousLogs`, `AllBatchLogs`, `AllChildLogs`, `AllImpersonatedLogs`, `AllRESTLogs`, `LogsToPurgeSoon`, `MyAssignedLogs`, `MyAssignedOpenLogs`, `MyGeneratedLogs`. +- **`LogEntry__c`**: `All`, `AllERRORLogEntries`, `AllERRORandWARNLogEntries`, `AllApexLogEntries`, `AllComponentLogEntries`, `AllFlowLogEntries`, `AllHttpRequestLogEntries`, `AllHttpResponseLogEntries`, `AllRestRequestLogEntries`, `AllRestResponseLogEntries`, `AllExceptionLogEntries`, `AllImpersonatedLogEntries`, `AllOmniStudioLogEntries`, `MyGeneratedLogEntries`. +- **`LogEntryTag__c`**: `All`, `AllImpersonatedLogEntryTags`. +- **`LoggerScenario__c`**: `All`, `MyAssignedLoggerScenarios`. +- **`LoggerTag__c`**: `All`. + +When investigating an incident, start with the pre-built views and narrow with column filters before creating a new custom list view. + +## Record Pages + +Nebula Logger ships a dedicated FlexiPage for each user-facing log-management object. The `LogEntryTag__c` junction is browsed through its record page but doesn't have its own FlexiPage in the log-management folder - it's rendered with the standard record page layout. + +### Log Record Page + +The `Log__c` record page (`LogRecordPage` FlexiPage) surfaces: + +- Standard `Log__c` fields (transaction ID, save method, user, scenario, tags, totals). +- The related `LogEntry__c` list, chronological within the log. +- Roll-up counts (`TotalERRORLogEntries__c`, `TotalWARNLogEntries__c`, etc.) to spot severity at a glance. +- Related child logs when a parent log transaction ID has been set (see the parent/child log association pattern documented in the Apex logging skill). + +### Log Entry Record Page + +The `LogEntry__c` record page (`LogEntryRecordPage` FlexiPage) shows: + +- Message, logging level, origin type / API name, and timestamp. +- Related record ID and record JSON snapshot when the entry was created with `.setRecord(...)`. +- Stack trace and exception fields when the entry came from an exception. +- Tags applied to the entry (via the `LogEntryTag__c` junction). + +### Log Entry Tag Record Page + +The `LogEntryTag__c` record page (`LogEntryTagRecordPage` FlexiPage) surfaces one row of the tag junction: the `LogEntry__c` it applies to and the `LoggerTag__c` being applied. Rarely opened directly - most tag investigation goes through the related lists on `LogEntry__c` or `LoggerTag__c` - but useful when auditing an impersonated user's tagging activity via `AllImpersonatedLogEntryTags`. + +### Logger Scenario Record Page + +The `LoggerScenario__c` record page (`LoggerScenarioRecordPage` FlexiPage) shows the scenario's persisted state after any transaction called `Logger.setScenario(...)`, along with related `Log__c` records that used it. Use this to answer "which transactions ran under scenario X, and who was assigned to follow up." + +### Logger Tag Record Page + +The `LoggerTag__c` record page (`LoggerTagRecordPage` FlexiPage) shows the tag catalog entry and its related `LogEntryTag__c` junctions, i.e. every log entry currently carrying this tag. Use this when auditing tag taxonomy usage or spotting typos / duplicates in the tag catalog. + +## Home Page and Live Streaming + +The console's home page (`LoggerHomePage` FlexiPage) is where the console's most operational tools live: + +- `logEntryEventStream` - subscribes to the `LogEntryEvent__e` platform event and shows log entries in real time as they are published. This is the fastest way to confirm logging is firing during a test scenario without waiting for the async save path to persist records. +- `logBatchPurge` - triggers the log purge batch on demand and reports counts. +- `loggerSettings` - the same settings editor exposed in the utility bar, on a full-page canvas. +- `loggerHomeHeader` - navigation header with quick links to release notes, GitHub, and the docs. +- Embedded reports and dashboards showing recent activity across `Log__c`, `LogEntry__c`, `LogEntryTag__c`, `LoggerScenario__c`, and `LoggerTag__c`. + +Together these give an admin one page to answer "is logging healthy right now, and what does it look like?". + +## Typical Workflows + +### Investigating a specific error + +1. Open the `AllERRORLogEntries` list view. +2. Filter to the time window of interest. +3. Open a `LogEntry__c` record - the linked `Log__c` shows the full transaction; the related list on the `Log__c` shows every entry in that transaction, in order. +4. If the transaction is part of an async chain, follow `ParentLog__c` upward to the parent transaction. + +### Investigating a user's session + +1. Open the `AllLogs` list view. +2. Filter by `LoggedBy__c` and time window. +3. Sort by `StartTime__c` to see the user's transactions in order. + +### Confirming logging is firing during a repro + +1. Open the Logger Console home page (`LoggerHomePage`). +2. Locate the `Log Entry Event Stream` component. +3. Reproduce the scenario in another tab. +4. Watch entries appear as `LogEntryEvent__e` platform events publish. When entries stop appearing, the async save has completed and the persisted records across `Log__c`, `LogEntry__c`, `LogEntryTag__c`, `LoggerScenario__c`, and `LoggerTag__c` are queryable. + +## Related Skills + +- [nebula-logger-instrumentation](../nebula-logger-instrumentation/SKILL.md) - Writing the log entries that show up here. +- [nebula-logger-purging-and-retention](../nebula-logger-purging-and-retention/SKILL.md) - How long records stay visible in these views. diff --git a/skills/nebula-logger-install/SKILL.md b/skills/nebula-logger-install/SKILL.md new file mode 100644 index 000000000..76cc841cd --- /dev/null +++ b/skills/nebula-logger-install/SKILL.md @@ -0,0 +1,116 @@ +--- +name: nebula-logger-install +description: Use this skill when the user wants to install and configure Nebula Logger in a Salesforce org for the first time. Covers package selection, installation paths, permissions, LoggerSettings__c hierarchy, and first-run troubleshooting. +--- + +# Nebula Logger Installation and Initial Configuration + +## Why Nebula Logger Instead of Only System.debug() + +`System.debug()` is useful during active debugging, but it is not a durable observability strategy for production operations. + +- Debug logs have short retention windows and are hard to query for historical analysis. +- Searching by business record, process scenario, or tags is limited compared to purpose-built logging objects. +- It does not provide a unified approach across Apex, LWC, Aura, Flow, and OmniStudio. + +Nebula Logger gives teams a consistent, queryable logging model across Salesforce runtime contexts. + +## Package Choice: Unlocked vs Managed + +Nebula Logger supports both package types with the same core metadata, but the operating model differs. + +| Area | Unlocked Package | Managed Package | +| ---------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| Namespace | None | `Nebula` | +| Release cadence | Faster patch cadence | Slower, stabilized cadence (roughly three managed releases per year) | +| Source visibility | Full source access in org | Packaged global API surface | +| Plugin framework | Available | Not currently available | +| Feature coverage | All features (plugins, latest fixes) | Subset - trails the unlocked package | +| Distribution model | GitHub-first OSS workflow | AppExchange-friendly for managed delivery | +| Typical recommendation | Default choice for almost every team | Only when a namespaced package is a hard requirement (ISV / AppExchange delivery, orgs that require namespace isolation) | + +Start with the unlocked package unless a namespaced package is a hard requirement. The unlocked package has more features (plugin framework, faster patches) and full source visibility; the managed package exists specifically for cases where the `Nebula` namespace is needed. + +## Installation Options + +Always confirm the latest version on: + +- https://github.com/jongpie/NebulaLogger/releases + +### Unlocked Package - v4.19.3 + +- Sandbox install link: `https://test.salesforce.com/packaging/installPackage.apexp?p0=04tg7000000IEirAAG` +- Production install link: `https://login.salesforce.com/packaging/installPackage.apexp?p0=04tg7000000IEirAAG` +- Salesforce CLI: `sf package install --wait 20 --security-type AdminsOnly --package 04tg7000000IEirAAG` + +### Managed Package - v4.19.0 + +- Sandbox install link: `https://test.salesforce.com/packaging/installPackage.apexp?mgd=true&p0=04tg7000000GZbJAAW` +- Production install link: `https://login.salesforce.com/packaging/installPackage.apexp?mgd=true&p0=04tg7000000GZbJAAW` +- Salesforce CLI: `sf package install --wait 30 --security-type AdminsOnly --package 04tg7000000GZbJAAW` + +## Permission Sets to Assign + +Assign permission sets as part of rollout. Exact API names in the repo are: + +| Permission Set | Purpose | Typical users | +| ------------------ | ------------------------------------------------------ | --------------------------------------------------- | +| `LoggerAdmin` | Full control of Nebula Logger data and features | Platform admins, support leads | +| `LoggerLogViewer` | Read-only access to logs and console features | Operations, QA, support analysts | +| `LoggerEndUser` | Limited day-to-day access with controlled visibility | Business users who need log visibility | +| `LoggerLogCreator` | Minimal metadata/object access needed to generate logs | Integration users, Experience Cloud component users | + +## Configure `LoggerSettings__c` Hierarchy + +Nebula Logger uses a hierarchy custom setting so behavior can be tuned at multiple scopes: + +1. Org default baseline. +2. Profile-level override. +3. User-level override. + +Prioritize these settings during onboarding: + +| Field | What it controls | +| ------------------------------------ | -------------------------------------------------------------------------- | +| `IsEnabled__c` | Global on/off switch for logging behavior at the effective hierarchy level | +| `LoggingLevel__c` | Effective minimum logging level for the user/profile/org context | +| `DefaultSaveMethod__c` | Default save strategy used by `Logger.saveLog()` | +| `DefaultLogPurgeAction__c` | Default cleanup behavior for old logs | +| `DefaultNumberOfDaysToRetainLogs__c` | Retention window for generated logs | + +Note: older references may mention `DefaultLoggingLevel__c`; in this codebase the active field is `LoggingLevel__c`. + +## Review and Customize `LoggerParameter__mdt` + +Nebula Logger ships a `LoggerParameter__mdt` CMDT populated with ~40 records that control cross-cutting runtime behavior - things like which supporting objects are queried, how tags are stored, whether the stack trace is parsed, and whether transaction limits get captured. The out-of-the-box values are the recommended defaults for most orgs, but they are opinionated defaults, and some orgs will want to adjust them. Review the list after install and change the ones that matter for your org. A few representative examples of the kinds of tradeoffs these records expose: + +- **SOQL-sensitive orgs**: if the extra queries Nebula Logger makes are a concern (e.g. very complex codebases that are already close to the 100-SOQL transaction limit), the `Query*Synchronously` records - `QueryUserDataSynchronously`, `QueryNetworkDataSynchronously`, `QueryOrganizationDataSynchronously`, `QueryAuthSessionDataSynchronously` - can be flipped to `false` so the enrichment queries only run async on the `Log__c` insert side. Setting the corresponding `Query*Data` record to `false` disables the query entirely if the extra fields aren't needed at all. +- **CPU-sensitive orgs**: `EnableStackTraceParsing` and `StoreHeapSizeLimit` are `true` by default. Flipping them to `false` skips the per-entry stack-trace parse and heap-limit capture, which trades some observability for lower CPU consumption on high-volume log paths. +- **Callout-restricted orgs**: `CallStatusApi` (async callout to `api.status.salesforce.com` for release info) can be set to `false` if outbound callouts to the Salesforce status endpoint aren't allowed or aren't wanted. +- **Feature toggles**: `EnableTagging`, `EnableLogEntryEventStream`, `NormalizeScenarioData`, `NormalizeTagData`, and similar records switch entire subsystems on or off - useful when a feature isn't needed and the associated storage / trigger work should be avoided. + +Every `LoggerParameter__mdt` record has an inline `Description__c` field explaining what it controls and how to change it; open the record in Setup or the Logger Console to see the guidance rather than trying to memorize the catalog. Treat any change to these records as a behavior change and re-run the affected tests afterwards - see the "Nebula Logger's CMDT Records Are Live in Tests" section of [nebula-logger-testing-your-code](../nebula-logger-testing-your-code/SKILL.md). + +## Review Data Masking with `LogEntryDataMaskRule__mdt` + +Nebula Logger also ships a small starter catalog of `LogEntryDataMaskRule__mdt` records that regex-mask sensitive data before it gets persisted: `SocialSecurityNumber`, `VisaCreditCardNumber`, `MastercardCreditCardNumber`, and `AmericanExpressCreditCardNumber`. Each record has: + +- `SensitiveDataRegEx__c` - the regex that matches the sensitive substring. +- `ReplacementRegEx__c` - the replacement pattern (typically preserves surrounding context and masks the middle digits). +- `ApplyToMessage__c` / `ApplyToRecordJson__c` - whether the rule runs against the log entry message text, against the `RecordJson__c` captured by `.setRecord(...)`, or both. +- `IsEnabled__c` - toggle without deleting the record. + +The shipped rules are a reasonable baseline but they're not exhaustive. Any org that logs domain data with its own sensitive patterns (customer IDs, national ID numbers outside the US, API tokens, internal case numbers, etc.) should deploy additional `LogEntryDataMaskRule__mdt` records for those patterns rather than relying on developers to remember to strip the values manually before calling `Logger.info(...)`. Test the regex against representative strings before shipping - a too-greedy pattern can mangle unrelated substrings, and a too-narrow pattern lets the sensitive value through. + +## First Troubleshooting Check + +If logs are not created after deployment, check permission set assignment first, especially `LoggerEndUser` (or `LoggerLogCreator` for component/integration contexts). Missing permissions are the most common post-install issue. + +## Next Steps + +Once Nebula Logger is installed and permissioned, most teams' next step is one of: + +- **Start logging** - see [nebula-logger-instrumentation](../nebula-logger-instrumentation/SKILL.md) for the APIs across Apex, LWC, Aura, Flow, and OmniStudio. +- **Set retention** - see [nebula-logger-purging-and-retention](../nebula-logger-purging-and-retention/SKILL.md) for `LogBatchPurger` scheduling and retention-day configuration. +- **Adopt team standards** - see [nebula-logger-best-practices](../nebula-logger-best-practices/SKILL.md) for environment-aware logging levels and review guardrails. +- **Investigate existing logs** - see [nebula-logger-console](../nebula-logger-console/SKILL.md) for the console app and its list views. diff --git a/skills/nebula-logger-instrumentation/SKILL.md b/skills/nebula-logger-instrumentation/SKILL.md new file mode 100644 index 000000000..4cd010ea0 --- /dev/null +++ b/skills/nebula-logger-instrumentation/SKILL.md @@ -0,0 +1,315 @@ +--- +name: nebula-logger-instrumentation +description: Use this skill when the user wants to add or update Nebula Logger instrumentation in Apex, LWC, Aura, Flow, or OmniStudio. Covers logging APIs, save patterns, record association, scenarios, tags, async transaction linking, and save method selection. +--- + +# Instrumenting Code with Nebula Logger + +## Supported API Surface + +Nebula Logger's supported API surface is everything marked `global` in Apex, and everything exported from the `c/logger` LWC module. The managed package (namespace `Nebula`) exposes only the `global` surface; the unlocked package technically exposes `public` classes and methods too, since there's no namespace boundary blocking access. + +**Do not rely on `public` Apex classes or methods from your own code.** They are internal to Nebula Logger and can change, be renamed, or be removed in any release without a deprecation window. `LoggerDataStore`, `LoggerConfigurationSelector`, `LogEntryHandler`, `LoggerPlugin` (the class itself, not the `LoggerPlugin__mdt` records), and everything else without a `global` modifier is subject to change without notice. + +If a capability you need isn't available through the `global` surface, file an issue at https://github.com/jongpie/NebulaLogger/issues rather than reaching into `public` methods. This skill and its companions only reference `global` APIs. + +## The Core Model + +Every Nebula Logger runtime context follows the same pattern: + +1. Add one or more log entries at a level (`ERROR`, `WARN`, `INFO`, `DEBUG`, `FINE`, `FINER`, `FINEST`). +2. Optionally enrich each entry with a record, exception details, or tags. +3. Persist the buffered entries by calling `saveLog()` once at the end of the transaction. + +The rest of this skill shows what that looks like in each runtime context. + +## Cross-Cutting Concepts + +### Logging levels + +Every context supports these seven levels: `ERROR`, `WARN`, `INFO`, `DEBUG`, `FINE`, `FINER`, `FINEST`. Which levels actually persist depends on the effective `LoggerSettings__c.LoggingLevel__c` for the user - see [nebula-logger-install](../nebula-logger-install/SKILL.md) for the hierarchy setup and [nebula-logger-best-practices](../nebula-logger-best-practices/SKILL.md) for level selection guidance. + +### Scenarios and tags + +Scenarios coarsely group a whole transaction under a business process; tags finely slice individual entries. Both are supported across every runtime. + +- Set the transaction scenario once with `setScenario('Order Fulfillment')`. +- Attach tags per-entry: chain `.addTag('inventory')` off the level-specific call. + +### Record association + +Prefer structured record association (`setRecord(...)`) over embedding IDs in message strings. Nebula Logger stores a JSON snapshot of the record so the log page can render the record's state at the time of the entry. + +### Async transaction linking + +Async work (batch, queueable, scheduled) runs in a new Apex transaction, which by default produces an unrelated `Log__c`. To link a child transaction back to its parent: + +- Capture the parent's transaction ID with `Logger.getTransactionId()`. +- Pass it to the child, then call `Logger.setParentLogTransactionId(parentId)` at the start of the child transaction. + +This creates a `ParentLog__c` link between the two `Log__c` records so investigation can walk the chain. + +## Apex + +Use level-specific methods to add entries, then persist once with `Logger.saveLog()`. + +```apex +public with sharing class InvoiceService { + public static void processInvoice(Id invoiceId) { + try { + Logger.info('Starting invoice processing for ' + invoiceId); + Logger.debug('Loading invoice and related records'); + + // Business logic here + + Logger.info('Invoice processing completed successfully'); + } catch (System.Exception ex) { + Logger.error('Invoice processing failed', ex); + throw ex; + } finally { + Logger.saveLog(); + } + } +} +``` + +### Exception logging + +`Logger.exception(...)` writes an `ERROR` log entry with the full exception context, saves the log, and rethrows the exception in one call. Do not add a separate `Logger.saveLog()` or `throw ex;` after it - both are redundant (and the `throw` is unreachable, since `Logger.exception(...)` already threw). + +```apex +try { + update accountsToUpdate; +} catch (System.DmlException ex) { + Logger.exception('Failed to update accounts', ex); +} +``` + +If you want to log the exception but decide separately whether to rethrow it, use `Logger.error(message, ex)` with an explicit `Logger.saveLog()` and `throw`: + +```apex +try { + update accountsToUpdate; +} catch (System.DmlException ex) { + Logger.error('Failed to update accounts', ex); + Logger.saveLog(); + throw ex; +} +``` + +### Attaching records + +```apex +Schema.Account acct = [SELECT Id, Name FROM Account WHERE Id = :accountId LIMIT 1]; + +Logger + .warn('Credit check returned warnings') + .setRecord(acct) + .addTag('credit-check') + .addTag('customer-onboarding'); + +Logger.saveLog(); +``` + +`setRecord(...)` is defined on `LogEntryEventBuilder` (returned by `Logger.info(...)` / `Logger.warn(...)` / etc.), not on `Logger` itself. Global overloads include `setRecord(SObject)`, `setRecord(Id)`, `setRecord(List)`, `setRecord(Map)`, `setRecord(System.Iterable)`, and `setRecordId(Id)`. Chain them off the level-specific call as shown above. + +### Async parent/child linking + +```apex +public with sharing class ParentQueueable implements System.Queueable { + public void execute(System.QueueableContext context) { + String parentTransactionId = Logger.getTransactionId(); + Logger.info('Parent job started'); + Logger.saveLog(); + + System.enqueueJob(new ChildQueueable(parentTransactionId)); + } +} + +public with sharing class ChildQueueable implements System.Queueable { + private final String parentLogTransactionId; + + public ChildQueueable(String parentLogTransactionId) { + this.parentLogTransactionId = parentLogTransactionId; + } + + public void execute(System.QueueableContext context) { + Logger.setParentLogTransactionId(this.parentLogTransactionId); + Logger.info('Child job linked to parent log transaction'); + Logger.saveLog(); + } +} +``` + +### Optional dependency via `CallableLogger` + +Code that wants to log to Nebula Logger _when it's installed_ but not hard-depend on it - for example, ISV packages that want to emit rich telemetry in customer orgs that happen to have Nebula Logger, without requiring every customer to install it - should call `CallableLogger` dynamically via `System.Callable` instead of referencing the `Logger` class directly. + +`CallableLogger` is `global` in both the unlocked and managed packages, and it exposes every logging operation as a string action with a `Map` input. Because the caller never compile-time-references `Logger`, the calling package compiles and installs cleanly whether Nebula Logger is present or not. + +```apex +public with sharing class OptionalLogger { + // An example of how to dynamically check first for the managed package ('Nebula' namespace), then the unlocked package (no namespace) + private static final System.Type CALLABLE_LOGGER_TYPE = System.Type.forName('Nebula', 'CallableLogger') ?? System.Type.forName('CallableLogger'); + + public static void info(String message) { + logEntry(System.LoggingLevel.INFO, message); + } + + public static void error(String message, System.Exception apexException) { + Map input = new Map{ + 'loggingLevel' => System.LoggingLevel.ERROR.name(), + 'message' => message, + 'exception' => apexException, + 'saveLog' => true + }; + invoke('newEntry', input); + } + + private static void logEntry(System.LoggingLevel level, String message) { + invoke('newEntry', new Map{ 'loggingLevel' => level.name(), 'message' => message, 'saveLog' => true }); + } + + private static void invoke(String action, Map input) { + if (CALLABLE_LOGGER_TYPE == null) { + return; + } + System.Callable logger = (System.Callable) CALLABLE_LOGGER_TYPE.newInstance(); + logger.call(action, input); + } +} +``` + +Callers of `OptionalLogger.info(...)` / `OptionalLogger.error(...)` get logging in orgs that have Nebula Logger installed and a no-op in orgs that don't - no runtime error, no compile-time dependency, no post-install step for the customer. + +#### Supported actions on `CallableLogger` + +Every action accepts a `Map` and returns a `Map` output (with keys `isSuccess`, `transactionId`, `parentLogTransactionId`, `requestId`, and error details on failure). + +| Action | Purpose | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `newEntry` | Add a log entry. Supports every enrichment key in the table below. | +| `saveLog` | Persist buffered entries. Optionally accepts `saveMethodName`. | +| `getTransactionId` | Return the current transaction ID (for async parent/child linking). | +| `getParentLogTransactionId` / `setParentLogTransactionId` | Read / set the parent transaction ID. | +| `getScenario` / `setScenario` / `endScenario` | Read / set / end the transaction's scenario. | +| `tryCatch` | Shorthand for `newEntry` with `loggingLevel=ERROR` and a serialized-input message; useful in generic `catch` blocks. | + +#### Supported input keys for `newEntry` + +The same input keys are used by both the ISV pattern above and OmniStudio's Remote Action steps. + +| Key | Purpose | +| -------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `loggingLevel` | Level name (`INFO`, `ERROR`, `WARN`, `DEBUG`, `FINE`, `FINER`, `FINEST`). | +| `message` | Entry message. | +| `saveLog` | Set to `true` to persist immediately after adding the entry. | +| `saveMethodName` | Override the save method for this call (`EVENT_BUS`, `QUEUEABLE`, `REST`, `SYNCHRONOUS_DML`). | +| `tags` | `List` of tag names. | +| `exception` | `System.Exception` instance to attach. | +| `recordId` / `record` / `recordList` / `recordMap` | Record association, same shape as the Apex `setRecord(...)` overloads. | +| `scenario` | Scenario for the transaction. | +| `parentLogTransactionId` | Parent transaction ID for async linking. | + +## LWC + +Import `getLogger` from the `c/logger` module and call it once per component instance. + +```js +import { LightningElement } from 'lwc'; +import { getLogger } from 'c/logger'; + +export default class PaymentPanel extends LightningElement { + logger = getLogger(); + + connectedCallback() { + this.logger.setScenario('Payment UI'); + this.logger.info('Payment panel initialized'); + this.logger.saveLog(); + } + + async handleSave() { + try { + this.logger.debug('Submitting payment request'); + // async work + this.logger.info('Payment submitted'); + } catch (error) { + this.logger.error('Payment submit failed').setExceptionDetails(error); + } finally { + await this.logger.saveLog(); + } + } +} +``` + +- `getLogger()` returns a scoped logger instance. Call it once and reuse. +- `saveLog()` is async - `await` it when the surrounding code needs the save to complete before returning. +- Level methods return a builder that supports `.setRecord(...)`, `.setExceptionDetails(...)`, `.setScenario(...)`, and `.addTag(...)`. + +## Aura + +Embed the `` component in markup and access it via `component.find('logger')`. + +```html + + + +``` + +```javascript +({ + handleRender: function (component) { + const logger = component.find('logger'); + logger.info('Aura component initialized'); + logger.saveLog(); + }, + + logButtonClick: function (component, event) { + const logger = component.find('logger'); + logger.info('Save button clicked').addTag('user-action'); + logger.saveLog(); + } +}); +``` + +Access the logger from `handleRender` or user-driven handlers rather than `doInit`. The embedded `` is an LWC-under-Aura, and LWC child methods are not yet available when `doInit` fires. If instrumentation must run at initialization time, wrap it in `setTimeout(..., 0)` so it runs after the render cycle. + +## Flow + +Nebula Logger ships three invocable actions (all in the `Logging` category) plus a save action: + +- `Add Log Entry` - Add a plain log entry with a message and logging level. +- `Add Log Entry for an SObject Record` - Add an entry linked to a single record. +- `Add Log Entry for an SObject Record Collection` - Add an entry linked to a collection of records. +- `Save Log` - Persist the buffered entries. End every flow path that added entries with this action. + +Flow variable references pass through the invocable inputs using standard merge syntax (`{!recordId}`, `{!Account.Name}`, etc.). Set the scenario on any `Add Log Entry*` action to group the whole flow's entries under one business process. + +## OmniStudio + +OmniStudio's OmniScripts and Integration Procedures log via the same `CallableLogger` class described under the Apex section's "Optional dependency" subsection. Instead of writing Apex, configure a **Remote Action** step in the OmniScript / IP: + +- `Remote Class`: `Nebula.CallableLogger` if the managed package is installed, otherwise `CallableLogger`. +- `Remote Method`: `newEntry` to add an entry (optionally saving in the same call), or `saveLog` to persist pending entries. See the "Supported actions" and "Supported input keys" tables in the Apex section above for the full menu. +- `Additional Input`: pass the input keys declaratively. Anything in the input map beyond the known keys is appended to the entry's message as an "OmniStudio Input" block, so process-specific context flows into the log without extra plumbing. + +`CallableLogger` automatically stamps `OriginType__c = 'OmniStudio'` and captures the `omniProcessId` from OmniStudio's default input so console filtering shows OmniStudio-sourced entries distinctly. + +## Save Method Selection Guide + +`Logger.SaveMethod` values are `EVENT_BUS`, `QUEUEABLE`, `REST`, and `SYNCHRONOUS_DML`. + +| Save Method | Use when | Trade-off | +| --------------------- | ------------------------------------------------------------- | ---------------------------------------------------- | +| `EVENT_BUS` (default) | General-purpose app logging | Depends on platform event capacity | +| `QUEUEABLE` | You want to defer work and reduce synchronous CPU pressure | Adds async dependency and queueable execution timing | +| `REST` | You need to avoid mixed-DML constraints in current context | Requires callout path and valid session context | +| `SYNCHRONOUS_DML` | You need immediate persistence and can tolerate rollback risk | Log inserts are rolled back if transaction fails | + +Override the default globally via `LoggerSettings__c.DefaultSaveMethod__c`, or per-transaction via `Logger.saveLog(Logger.SaveMethod.QUEUEABLE)`. + +## Related Skills + +- [nebula-logger-testing-your-code](../nebula-logger-testing-your-code/SKILL.md) - Testing code that calls the APIs above. +- [nebula-logger-console](../nebula-logger-console/SKILL.md) - Browsing the log records these APIs produce. +- [nebula-logger-best-practices](../nebula-logger-best-practices/SKILL.md) - Team-wide instrumentation standards. diff --git a/skills/nebula-logger-plugin-development/SKILL.md b/skills/nebula-logger-plugin-development/SKILL.md new file mode 100644 index 000000000..cd5010d63 --- /dev/null +++ b/skills/nebula-logger-plugin-development/SKILL.md @@ -0,0 +1,123 @@ +--- +name: nebula-logger-plugin-development +description: Use this skill when the user wants to build a new plugin that extends Nebula Logger - for example, custom trigger handlers on `LogEntryEvent__e` / `Log__c` / `LogEntry__c`, custom purge actions, or Slack-style outbound integrations. Covers the plugin framework interfaces, `LoggerPlugin__mdt` configuration, package layout, and testing considerations. +--- + +# Building Plugins for Nebula Logger + +## Package Compatibility + +The plugin framework is **only available in the unlocked package**. The managed package (namespace `Nebula`) does not currently expose the plugin extension points. + +If you plan to write plugins, install the unlocked package. See [nebula-logger-install](../nebula-logger-install/SKILL.md) for the choice. + +Note that `LoggerPlugin`, `LoggerPlugin.Triggerable`, `LoggerPlugin.Batchable`, `LoggerTriggerableContext`, and `LoggerBatchableContext` are all `public` (not `global`). Everywhere else in this skill collection, the guidance is "only rely on `global`" (see the "Supported API Surface" section of [nebula-logger-instrumentation](../nebula-logger-instrumentation/SKILL.md)). The plugin framework is a deliberate exception: the extension points are `public` because they're only reachable inside the unlocked package's own compilation unit. As with any `public` surface, breaking changes are possible - pin plugins to a Nebula Logger version you've tested against and re-verify after upgrades. + +## Plugin Framework Overview + +Nebula Logger's plugin framework lets you register Apex classes to run inside two extension points: + +1. **Trigger extension point** - Runs during `LoggerSObjectHandler` execution on `LogEntryEvent__e`, `Log__c`, `LogEntry__c`, `LogEntryTag__c`, `LoggerScenario__c`, and `LoggerTag__c` triggers. Use this to add fields, enrich data, or send external notifications when a log is created. +2. **Batch extension point** - Runs during `LogBatchPurger` execution. Use this to archive logs to an external system, apply custom purge policies, or emit metrics on purged records before they're deleted. + +Both extension points are driven by `LoggerPlugin__mdt` records. Adding a plugin means: writing an Apex class that implements the right interface (or both, if the plugin has trigger and batch responsibilities), then creating a `LoggerPlugin__mdt` record that points at it. + +**Strongly prefer Apex over Flow for both extension points.** The framework technically supports Flow-based plugins via `SObjectHandlerFlowName__c` / `BatchPurgerFlowName__c`, but Flow support currently has known issues with no fixed ETA - it may be deprecated altogether in a future release. New plugins should implement the Apex interfaces below and leave the Flow fields on the `LoggerPlugin__mdt` record blank. + +## The Two Interfaces + +Both interfaces live on `LoggerPlugin` in the `Nebula Logger - Core` package. + +### `LoggerPlugin.Triggerable` (trigger extension point) + +Implement one method: + +```apex +void execute(LoggerPlugin__mdt configuration, LoggerTriggerableContext input); +``` + +- `configuration` is the `LoggerPlugin__mdt` record that registered the plugin - use it to read plugin-specific settings. +- `input` describes the current trigger context and exposes `sobjectType`, `triggerOperationType`, `triggerNew`, `triggerNewMap`, `triggerOldMap`, and a `triggerRecords` list that pairs old/new record versions per record (populated for Flow-friendly access). + +### `LoggerPlugin.Batchable` (batch extension point) + +Implement three methods matching the `Database.Batchable` lifecycle: + +```apex +void start(LoggerPlugin__mdt configuration, LoggerBatchableContext input); +void execute(LoggerPlugin__mdt configuration, LoggerBatchableContext input, List scopeRecords); +void finish(LoggerPlugin__mdt configuration, LoggerBatchableContext input); +``` + +`scopeRecords` is the current batch of records `LogBatchPurger` is about to process. `LogBatchPurger` walks the object hierarchy from the lowest level up (`LogEntryTag__c`, then `LogEntry__c`, then `Log__c`), filtered to records whose parent `Log__c.LogRetentionDate__c` is past-due. Check `input.sobjectType` if the plugin behavior needs to differ per object. + +## `LoggerPlugin__mdt` Configuration + +Every plugin gets one `LoggerPlugin__mdt` record. Key fields: + +| Field | Purpose | +| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `DeveloperName` / `Label` | Identify the plugin. | +| `IsEnabled__c` | Toggle without deleting the record. Marked as `SubscriberControlled` so admins can flip it in installed orgs. | +| `SObjectHandlerApexClass__c` | Apex class name implementing `LoggerPlugin.Triggerable`. Set this to register the class at the trigger extension point. | +| `SObjectHandlerFlowName__c` | Flow API name for a Flow-based trigger plugin. Leave null - see the "prefer Apex" note above. | +| `SObjectHandlerExecutionOrder__c` | Integer that orders multiple plugins on the same handler. Lower numbers run first. Ties are broken by developer name. | +| `BatchPurgerApexClass__c` | Apex class name implementing `LoggerPlugin.Batchable`. Set this to register the class at the batch extension point. | +| `BatchPurgerFlowName__c` | Flow API name for a Flow-based batch plugin. Leave null - see the "prefer Apex" note above. | +| `BatchPurgerExecutionOrder__c` | Ordering across multiple batch plugins. | +| `Description__c` / `Link__c` / `VersionNumber__c` | Metadata surfaced in the Logger Console for admin awareness. | + +Set whichever combination of the Apex class fields matches what the class implements: `SObjectHandlerApexClass__c` for a `LoggerPlugin.Triggerable`, `BatchPurgerApexClass__c` for a `LoggerPlugin.Batchable`, or both fields pointing at the same class when it implements both interfaces (like `LogEntryArchivePlugin`). Leave the Flow fields null. + +## Plugin Folder Layout + +Existing plugins under `nebula-logger/plugins//` follow a consistent layout. When scaffolding a new plugin, mirror it: + +``` +nebula-logger/plugins// + plugin/ + classes/ # Apex classes (both implementation and *_Tests.cls test classes) + customMetadata/ # LoggerPlugin..md-meta.xml + objects/ # Any plugin-specific custom objects/fields (optional) + permissionsets/ # PluginAdmin.permissionset-meta.xml (optional) + testSuites/ # Plugin.testSuite-meta.xml grouping the plugin's test classes + README.md +``` + +Test classes live in the same `plugin/classes/` folder as the code under test - they aren't split into a separate top-level `tests/` folder. The Slack plugin's `plugin/slack/` subfolder is a plugin-specific quirk from packaging around some shared `core/main` metadata; new plugins should drop the extra nesting and put everything directly under `plugin/`. + +The `LoggerPlugin..md-meta.xml` file is the registration record - populate `SObjectHandlerApexClass__c` and/or `BatchPurgerApexClass__c` per the guidance above. Existing plugins to reference: + +- `nebula-logger/plugins/slack/` - trigger plugin that sends Slack notifications. +- `nebula-logger/plugins/log-retention-rules/` - trigger plugin that sets `LogRetentionDate__c` based on CMDT rules. +- `nebula-logger/plugins/big-object-archiving/` - implements BOTH interfaces on a single class (`LogEntryArchivePlugin`) - the trigger side captures records to the big object, the batch side archives before purge. +- `nebula-logger/plugins/async-failure-additions/` - adds logging for `FlowExecutionErrorEvent`, unexpected batch failures, and Queueable finalizers. + +## Ordering and Composition + +- Trigger plugins run **after** the built-in `LoggerSObjectHandler` logic for that object. +- Multiple plugins on the same handler run in `SObjectHandlerExecutionOrder__c` order (nulls last, then by `DeveloperName`). +- Plugins that mutate the trigger records (adding tags, populating fields) should run before plugins that publish outbound notifications, so notifications see the enriched data. + +## Testing Plugins + +- Ship each plugin with a matching `_Tests.cls` alongside the implementation class in `plugin/classes/`. +- Add the plugin's test classes to a `Plugin.testSuite-meta.xml` under `plugin/testSuites/` (see `LoggerLogEntryArchivePlugin.testSuite-meta.xml` and `LoggerLogRetentionRulesPlugin.testSuite-meta.xml` for the pattern). The suite becomes the target of a `sf apex run test --suite-names Plugin` invocation. Only the core package currently has a dedicated `npm run test:apex:suite:core` script; plugin suites are exercised through `npm run test:apex` (which runs `RunLocalTests`) or an ad-hoc `sf apex run test --suite-names` call. +- For trigger plugins, construct a `LoggerTriggerableContext` manually with the trigger records you want to exercise, then call `execute(configuration, input)` directly. Do not require a real trigger fire in tests. +- For batch plugins, construct a `LoggerBatchableContext` and pass a synthesized `scopeRecords` list. Assert on the side effects (DML, callouts via `System.Test.setMock`, published events). + +## New Plugin Checklist + +Before shipping a new plugin: + +1. Apex class implements at least one of `LoggerPlugin.Triggerable` / `LoggerPlugin.Batchable` - both is fine when a single class needs to react to trigger events and participate in the purge batch. +2. `LoggerPlugin..md-meta.xml` exists, has `IsEnabled__c=true` (or `false` if opt-in), and populates the class-name field(s) that match the interfaces the class implements: `SObjectHandlerApexClass__c` for `Triggerable`, `BatchPurgerApexClass__c` for `Batchable`, or both fields when the class implements both interfaces. +3. `PluginAdmin.permissionset-meta.xml` grants full access on plugin-owned objects and CMDT. +4. `_Tests.cls` covers happy path, empty scope, and disabled-configuration cases. +5. `README.md` in the plugin folder documents install steps and any prerequisites. +6. `sfdx-project.json` has a `packageDirectories` entry for the plugin so it can be built and installed as its own unlocked package. + +## Related Skills + +- [nebula-logger-console](../nebula-logger-console/SKILL.md) - The `LoggerPlugin__mdt` list view in the console is where admins toggle plugins on and off. +- [nebula-logger-purging-and-retention](../nebula-logger-purging-and-retention/SKILL.md) - Where `LoggerPlugin.Batchable` plugins slot in. diff --git a/skills/nebula-logger-purging-and-retention/SKILL.md b/skills/nebula-logger-purging-and-retention/SKILL.md new file mode 100644 index 000000000..117732f75 --- /dev/null +++ b/skills/nebula-logger-purging-and-retention/SKILL.md @@ -0,0 +1,87 @@ +--- +name: nebula-logger-purging-and-retention +description: Use this skill when the user wants to configure how long Nebula Logger keeps log records before deleting or archiving them. Covers retention date semantics on `Log__c`, the `LogBatchPurger` batch job, `LogBatchPurgeScheduler`, purge action values, and how to tune retention per user, profile, or scenario. +--- + +# Log Retention and Purging in Nebula Logger + +## The Retention Model + +Every `Log__c` record has a `LogRetentionDate__c`. When that date passes, the log becomes eligible for purging by `LogBatchPurger`. The retention date is set at insert time on `Log__c` from a combination of: + +- `LoggerSettings__c.DefaultNumberOfDaysToRetainLogs__c` on the effective hierarchy record. +- Optionally, `LoggerScenarioRule__mdt.NumberOfDaysToRetainLogs__c` when the log's scenario matches a scenario rule (scenario-specific overrides win over the hierarchy default). + +If no retention days are configured, `LogRetentionDate__c` is left null and the log is never auto-purged. + +## `LoggerSettings__c` Retention Fields + +The retention side of `LoggerSettings__c` is a hierarchy custom setting - values cascade org default -> profile -> user. The relevant fields: + +| Field | Purpose | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| `DefaultNumberOfDaysToRetainLogs__c` | Days between log insert and eligibility for purging. Null = no automatic purge. | +| `DefaultLogPurgeAction__c` | What the batch job does when it processes an eligible log. Default value is `Delete`. | +| `IsEnabled__c` | Master switch; disabling logging at a hierarchy level also stops new `Log__c` records from being written for those users. | + +## Purge Actions + +`Log__c.LogPurgeAction__c` (populated from `LoggerSettings__c.DefaultLogPurgeAction__c` at insert time) tells `LogBatchPurger` what to do when a log ages out: + +- **`Delete`** (default): The batch job hard-deletes the `Log__c` and its child `LogEntry__c` records. They do not sit in the Recycle Bin. +- Plugin-provided actions: Some Nebula Logger plugins add their own purge actions. For example, the `big-object-archiving` plugin copies logs into a big object before deletion so the data remains queryable long-term. When such a plugin is installed, its purge action becomes a valid value for `LogPurgeAction__c`. + +## Scheduling the Purge Job + +`LogBatchPurger` does not run automatically. Schedule it via `LogBatchPurgeScheduler`: + +```apex +// Run daily at 2am - adjust the cron expression for your org +String cronExpression = '0 0 2 * * ?'; +System.schedule('Nebula Logger - Daily Purge', cronExpression, new LogBatchPurgeScheduler()); +``` + +`LogBatchPurgeScheduler` has a no-arg constructor for default batch size and a `LogBatchPurgeScheduler(Integer batchSize)` constructor for tuning throughput. + +Without a scheduled job, `LogRetentionDate__c` still gets set correctly on new logs, but nothing ever purges them. An unscheduled purger is the most common cause of "we set retention to 30 days but logs from six months ago are still there." + +## Per-Scenario Retention + +To keep certain business processes' logs longer (or shorter) than the default, create a `LoggerScenarioRule__mdt` record whose `Scenario__c` matches the value passed to `Logger.setScenario(...)`. Set: + +- `IsEnabled__c` = `true` (otherwise the rule is inert). +- `IsLogRetentionOverrideEnabled__c` = `true` (this is the gate that actually enables the retention override - a `NumberOfDaysToRetainLogs__c` value with this flag off does nothing). +- `NumberOfDaysToRetainLogs__c` = the target retention in days for logs matching this scenario. + +Use scenario rules for regulated processes ("keep financial-audit logs for 7 years") or for high-volume test scenarios ("purge integration-smoke-test logs after 24 hours") rather than trying to tune the hierarchy globally. + +## Per-User / Per-Profile Retention + +Because `LoggerSettings__c` is a hierarchy custom setting, retention can be tuned per user or per profile. Common patterns: + +- **Debug users retain longer**: Give developer profiles a longer `DefaultNumberOfDaysToRetainLogs__c` so investigation work isn't lost. +- **High-volume integration users retain shorter**: Give integration user profiles a shorter retention so their (much larger) log volume doesn't dominate storage. + +Set the profile-level value in `LoggerSettings__c` for those profiles; it inherits from org default for everyone else. + +## Verifying Retention Setup + +Before assuming retention is working, verify these three things in the target org: + +1. `LoggerSettings__c` at the org default level has a non-null `DefaultNumberOfDaysToRetainLogs__c`. +2. `LogBatchPurgeScheduler` is scheduled (check Setup -> Scheduled Jobs). +3. New `Log__c` records get a `LogRetentionDate__c` populated. Run a quick test log and query `SELECT Id, LogRetentionDate__c FROM Log__c ORDER BY CreatedDate DESC LIMIT 1`. + +If any of those three is missing, retention is not actually in effect. + +## Storage Considerations + +- `Log__c` and `LogEntry__c` count against custom object storage. `LogEntry__c` is typically the volume driver (many entries per log). +- Higher `LoggerSettings__c.LoggingLevel__c` values (`FINE`, `FINER`, `FINEST`) produce dramatically more entries. Combine environment-appropriate logging levels with retention windows to control storage growth - see [nebula-logger-best-practices](../nebula-logger-best-practices/SKILL.md). +- If storage becomes a real constraint, either shorten retention, raise logging level thresholds in production, or install the `big-object-archiving` plugin to offload aged data to a big object. + +## Related Skills + +- [nebula-logger-install](../nebula-logger-install/SKILL.md) - Initial `LoggerSettings__c` configuration. +- [nebula-logger-best-practices](../nebula-logger-best-practices/SKILL.md) - Environment-aware logging levels that pair with retention. +- [nebula-logger-plugin-development](../nebula-logger-plugin-development/SKILL.md) - Building a custom purge-action plugin (e.g. archive to an external system). diff --git a/skills/nebula-logger-testing-your-code/SKILL.md b/skills/nebula-logger-testing-your-code/SKILL.md new file mode 100644 index 000000000..a7d8dbcb6 --- /dev/null +++ b/skills/nebula-logger-testing-your-code/SKILL.md @@ -0,0 +1,183 @@ +--- +name: nebula-logger-testing-your-code +description: Use this skill when the user wants to write Apex or LWC tests for code that calls Nebula Logger. Covers observing that the right entries were buffered, controlling logging levels inside tests, isolating tests from persisted `Log__c` / `LogEntry__c` / `LogEntryTag__c` / `LoggerScenario__c` / `LoggerTag__c` records, and Nebula Logger APIs that are safe to call from a subscriber's own test suite. +--- + +# Testing Code That Uses Nebula Logger + +This skill is for developers whose own Apex or LWC code calls `Logger.info(...)`, `Logger.error(...)`, etc. and who want to prove in tests that the right things get logged. It only uses `global` Nebula Logger APIs, so the same techniques work whether the org has the unlocked or the managed package installed. + +## The Buffer Model + +Nebula Logger buffers log entries in memory during a transaction. `Logger.saveLog()` moves the buffered entries onto the save path (platform event, queueable, REST, or synchronous DML) - see [nebula-logger-instrumentation](../nebula-logger-instrumentation/SKILL.md) for the save method choice. + +For tests, the buffer is the observation point: you can add entries, inspect the buffer size before `saveLog()`, then either call `saveLog()` and query the resulting records, or `flushBuffer()` to drop them without persisting. All three APIs are global. + +## Assert on the Buffer Before Save + +`Logger.getBufferSize()` returns the number of entries currently buffered. Use it to prove your code added the entries you expected without needing to persist them. + +```apex +@IsTest +static void it_should_log_a_warning_when_credit_check_fails() { + Account account = new Account(Name = 'Test Account'); + insert account; + + System.Test.startTest(); + new CreditCheckService().evaluate(account.Id); + System.Test.stopTest(); + + System.Assert.areEqual(1, Logger.getBufferSize(), 'Expected exactly one entry buffered'); +} +``` + +Because `getBufferSize()` runs before `saveLog()` fires, the assertion works without waiting for the platform event to publish. + +## Assert on Persisted Records After Save + +When you need to assert on the message text, logging level, related record, or scenario, save the log and query the persisted records. The full set of custom objects on the log-management side is: + +- `Log__c` - one record per logging transaction. +- `LogEntry__c` - one record per `Logger.info(...)` / `.error(...)` / etc. call. +- `LogEntryTag__c` - junction between `LogEntry__c` and `LoggerTag__c` when tags are applied. +- `LoggerScenario__c` - captures each unique `Logger.setScenario(...)` value used across transactions. +- `LoggerTag__c` - the tag catalog referenced by `LogEntryTag__c`. + +With the default `EVENT_BUS` save method, the platform event fires during `System.Test.stopTest()`, populating all of the above. + +### When You Also Need `System.Test.getEventBus().deliver()` + +`System.Test.stopTest()` only delivers the platform events queued up **before** the stopTest boundary. Any `LogEntryEvent__e` records published **during** the async work that stopTest itself flushes need a follow-up call to `System.Test.getEventBus().deliver()` before `LogEntry__c` / `Log__c` records materialize. The two situations where this matters: + +- **`Logger.setSaveMethod(Logger.SaveMethod.QUEUEABLE)` or `Logger.saveLog(Logger.SaveMethod.QUEUEABLE)`** - Nebula Logger's queueable-based save publishes the platform event from inside the queueable. The queueable runs during `stopTest`, but the event it publishes needs a second delivery pass. +- **Async code under test that itself logs** - A `System.Queueable`, `System.Finalizer`, or `Database.Batchable` that calls `Logger.info(...)` / `.error(...)` / `saveLog()` inside `execute` publishes its platform event from within the async context. `stopTest` runs the async job but does not deliver the platform events it produced. + +```apex +@IsTest +static void it_should_log_the_error_from_a_failed_queueable() { + System.Test.startTest(); + System.enqueueJob(new ExampleFailedQueueable()); + System.Test.stopTest(); + System.Test.getEventBus().deliver(); + + List entries = [SELECT LoggingLevel__c FROM LogEntry__c WHERE LoggingLevel__c = 'ERROR']; + System.Assert.areEqual(1, entries.size()); +} +``` + +For synchronous code using the default `EVENT_BUS` save method, `stopTest` alone is enough - no extra deliver call is needed. + +```apex +@IsTest +static void it_should_log_the_error_message_when_credit_check_throws() { + Account account = new Account(Name = 'Test Account'); + insert account; + + System.Test.startTest(); + System.Exception thrownException; + try { + new CreditCheckService().evaluate(account.Id); + System.Assert.fail('Expected an exception'); + } catch (System.Exception ex) { + thrownException = ex; + } + System.Test.stopTest(); + + System.Assert.areNotEqual('Expected an exception', thrownException.getMessage(), 'The wrong exception was thrown!'); + List entries = [ + SELECT LoggingLevel__c, Message__c, RecordId__c, ExceptionMessage__c, ExceptionType__c + FROM LogEntry__c + ]; + System.Assert.areEqual(1, entries.size()); + System.Assert.areEqual('ERROR', entries[0].LoggingLevel__c); + System.Assert.areEqual(thrownException.getMessage(), entries[0].ExceptionMessage__c); + System.Assert.areEqual(thrownException.getTypeName(), entries[0].ExceptionType__c); +} +``` + +## Control Whether Entries Are Persisted + +By default, Nebula Logger honors `LoggerSettings__c` inside test context - if the org default has logging enabled at an appropriate level, entries persist. If your test wants to run the code under test without touching the database: + +- Call `Logger.suspendSaving()` at the top of the test. Subsequent `saveLog()` calls are no-ops - no records are inserted, no platform events are published, and the buffer is left untouched. +- Call `Logger.resumeSaving()` to reverse the suspend. Every entry that accumulated in the buffer while saving was suspended is still there, so the next `saveLog()` call flushes all of them. If the intent is to discard those entries instead of persisting them, call `Logger.flushBuffer()` before resuming. +- Call `Logger.flushBuffer()` at the top of the test if you only want to prevent test-triggered entries from accumulating before your setup completes. +- Call `Logger.isSavingSuspended()` to guard against test pollution when writing shared test utilities. + +```apex +@IsTest +static void it_should_not_persist_log_records_when_saving_is_suspended() { + Logger.suspendSaving(); + + new CreditCheckService().evaluate(fakeAccountId()); + Logger.saveLog(); + + System.Assert.isTrue([SELECT COUNT() FROM Log__c] == 0); +} +``` + +## Nebula Logger's CMDT Records Are Live in Tests + +Salesforce returns the org's actual custom metadata records when tests query CMDT - the platform does not roll them back like it does with sObject data. Nebula Logger takes advantage of that: the internal selector layer reads whatever `LoggerParameter__mdt`, `LogEntryDataMaskRule__mdt`, `LogEntryTagRule__mdt`, `LoggerFieldMapping__mdt`, `LoggerPlugin__mdt`, `LoggerScenarioRule__mdt`, `LoggerSObjectHandler__mdt`, and `LogStatus__mdt` records are deployed to the org, and uses them during tests exactly the way it uses them at runtime. + +Practical consequences for a subscriber's own tests: + +- Changing a `LoggerParameter__mdt.Value__c` in the org (e.g. flipping `QueryUserDataSynchronously` from `true` to `false`, or setting a non-Boolean value on a parameter that expects a Boolean) affects test behavior, not just runtime behavior. Tests that assumed the out-of-the-box value can start failing after a config change. +- New `LogEntryDataMaskRule__mdt` records you deploy will mask sensitive data inside tests, so an assertion against the raw message text needs to expect the masked form. +- New `LoggerPlugin__mdt` records execute in tests too. If a plugin has side effects (callouts, DML, platform events), tests need to mock or account for them. +- `LoggerScenario__mdt` records override `LoggerSettings__c` at test time the same way they do at runtime. + +Treat CMDT changes as behavior changes, and rerun the relevant test suite after deploying any customization to Nebula Logger's CMDT records. Nebula Logger's internal `LoggerConfigurationSelector.useMocks()` seam is not `global`, so it isn't available for subscriber tests - the supported approach is to keep tests aware of the org's CMDT state, and use `Logger.suspendSaving()` / `flushBuffer()` when a test needs to opt out of the persistence side of things. + +## LWC Test Considerations + +For LWC tests using `sfdx-lwc-jest`, mock the `c/logger` module and assert that your component calls the right entry-level and passes the right message / record. + +```js +// __tests__/paymentPanel.test.js +import { createElement } from 'lwc'; +import PaymentPanel from 'c/paymentPanel'; + +const mockLogger = { + setScenario: jest.fn(), + info: jest.fn().mockReturnThis(), + error: jest.fn().mockReturnThis(), + setExceptionDetails: jest.fn().mockReturnThis(), + saveLog: jest.fn() +}; + +jest.mock('c/logger', () => ({ + getLogger: () => mockLogger +})); + +describe('c-payment-panel', () => { + it('logs an INFO entry when the panel initializes', () => { + const element = createElement('c-payment-panel', { is: PaymentPanel }); + document.body.appendChild(element); + + expect(mockLogger.setScenario).toHaveBeenCalledWith('Payment UI'); + expect(mockLogger.info).toHaveBeenCalledWith('Payment panel initialized'); + expect(mockLogger.saveLog).toHaveBeenCalled(); + }); +}); +``` + +Per the repo's testing conventions, assert on the arguments passed to the logger methods (`toHaveBeenCalledWith(...)`), not just that they were called. `toHaveBeenCalled()` alone lets regressions slip through where the message text is dropped or garbled. + +## Global APIs Safe for Test Code + +Only `global` APIs are supported for use from your own test code. Anything `public` is Nebula Logger's internal surface and can change without notice - see the "Supported API Surface" section in [nebula-logger-instrumentation](../nebula-logger-instrumentation/SKILL.md). + +The following are all `global` and safe to call from a subscriber's test suite in either package: + +- `Logger.getBufferSize()` - Inspect how many entries are buffered. +- `Logger.suspendSaving()` / `Logger.resumeSaving()` / `Logger.isSavingSuspended()` - Prevent or gate persistence. +- `Logger.flushBuffer()` - Drop buffered entries without persisting. +- `Logger.getTransactionId()` - Correlate a test's logs when asserting on persisted records. +- `Logger.setScenario(...)` / `Logger.endScenario(...)` - Set a test-specific scenario so persisted logs are easy to identify. +- Level-specific methods (`Logger.info(...)`, `Logger.error(...)`, etc.) and `Logger.saveLog()`. + +## Related Skills + +- [nebula-logger-instrumentation](../nebula-logger-instrumentation/SKILL.md) - The APIs being tested. +- [nebula-logger-best-practices](../nebula-logger-best-practices/SKILL.md) - Conventions the code under test should follow.