Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.cls": "${capture}.cls-meta.xml",
"*.css": "${capture}.min.css",
"*.js": "${capture}.min.js",
"*.page": "${capture}.page-meta.xml",
"*.trigger": "${capture}.trigger-meta.xml",
"*.view": "${capture}.view-meta.xml",
Expand Down
7 changes: 7 additions & 0 deletions .forceignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ nebula-logger/managed-package/**/*.testSuite-meta.xml
**/tsconfig.json

**/*.ts


# PrismJS - only the minified files are deployed, the unminified versions are
# only kept in source control to make it easier to see what's changed when upgrading PrismJS.
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/prism.js
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/themes/prism-tomorrow.css
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/README.md
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.sf/
.sfdx/
.vscode/
.claude/
docs/apex/Miscellaneous/
temp/
test-coverage/
Expand Down
10 changes: 5 additions & 5 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ yarn.lock
*.log
*.xml

# Prism JS
# The CSS for Prism has specifically been modified for Nebula Logger (a bit), so it's NOT ignored.
# nebula-logger/core/main/log-management/staticresources/LoggerResources/prism.css
# The minified JS is taken as-is, so no need for prettier to format it.
nebula-logger/core/main/log-management/staticresources/LoggerResources/prism.js
# PrismJS - these files are automatically updated & minified,
# so no need for prettier to format them.
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/prism.js
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/prism.min.js
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/themes/*.*
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

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.

## Unlocked Package - v4.18.2
## Unlocked Package - v4.18.3

[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tg70000008YZBAA2)
[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tg70000008YZBAA2)
[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tg70000009GaDAAU)
[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tg70000009GaDAAU)
[![View Documentation](./images/btn-view-documentation.png)](https://github.com/jongpie/NebulaLogger/wiki)

`sf package install --wait 20 --security-type AdminsOnly --package 04tg70000008YZBAA2`
`sf package install --wait 20 --security-type AdminsOnly --package 04tg70000009GaDAAU`

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jest.mock(
return {
loadScript() {
return new Promise(resolve => {
global.Prism = require('../../../staticresources/LoggerResources/prism.js');
global.Prism = require('../../../staticresources/LoggerResources/Prism/prism.min.js');
resolve();
});
},
Expand All @@ -31,6 +31,40 @@ jest.mock(
{ virtual: true }
);

// Number of microtask hops to drain across getRecord, c-logger-code-viewer creation,
// and the Promise.all(map(async)) chain inside _loadPrismResources.
const flushPromises = async () => {
for (let i = 0; i < 8; i++) {
/* eslint-disable-next-line no-await-in-loop */
await Promise.resolve();
}
};

const buildSnippet = overrides => ({
Code: 'some-code-block',
ApiVersion: '65.0',
TotalLinesOfCode: 123,
StartingLineNumber: 55,
TargetLineNumber: 65,
EndingLineNumber: 68,
...overrides
});

const buildLogEntryRecord = ({ source, metadataType, snippet }) => {
const apiNameField = source === 'Exception' ? 'ExceptionSourceApiName__c' : 'OriginSourceApiName__c';
const apiVersionField = source === 'Exception' ? 'ExceptionSourceApiVersion__c' : 'OriginSourceApiVersion__c';
const metadataTypeField = source === 'Exception' ? 'ExceptionSourceMetadataType__c' : 'OriginSourceMetadataType__c';
const snippetField = source === 'Exception' ? 'ExceptionSourceSnippet__c' : 'OriginSourceSnippet__c';
return {
fields: {
[apiNameField]: { value: 'SomeApexClass' },
[apiVersionField]: { value: '65.0' },
[metadataTypeField]: { value: metadataType },
[snippetField]: { value: snippet === null ? null : JSON.stringify(snippet) }
}
};
};

describe('LogEntryMetadataViewer LWC Tests', () => {
afterEach(() => {
while (document.body.firstChild) {
Expand Down Expand Up @@ -65,9 +99,7 @@ describe('LogEntryMetadataViewer LWC Tests', () => {

document.body.appendChild(element);
getRecord.emit(mockLogEntryRecord);
await Promise.resolve('resolves getRecord() call');
await Promise.resolve('resolves creating an instance of c-logger-code-viewer');
await Promise.resolve('resolves loading & running PrismJS inside of c-logger-code-viewer');
await flushPromises();

const sectionTitle = element.shadowRoot.querySelector('c-logger-page-section span[slot="title"]');
expect(sectionTitle).toBeTruthy();
Expand Down Expand Up @@ -116,9 +148,7 @@ describe('LogEntryMetadataViewer LWC Tests', () => {

document.body.appendChild(element);
getRecord.emit(mockLogEntryRecord);
await Promise.resolve('resolves getRecord() call');
await Promise.resolve('resolves creating an instance of c-logger-code-viewer');
await Promise.resolve('resolves loading & running PrismJS inside of c-logger-code-viewer');
await flushPromises();

const sectionTitle = element.shadowRoot.querySelector('c-logger-page-section span[slot="title"]');
expect(sectionTitle).toBeTruthy();
Expand All @@ -140,4 +170,184 @@ describe('LogEntryMetadataViewer LWC Tests', () => {
expect(viewFullSourceButton.label).toBe('View Full Source');
expect(viewFullSourceButton.variant).toBe('inverse');
});

it('should show a spinner before the wired log entry record loads', async () => {
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
element.sourceMetadata = 'Exception';
element.recordId = 'test-log-entry-id';

document.body.appendChild(element);
await flushPromises();

expect(element.shadowRoot.querySelector('lightning-spinner')).toBeTruthy();
expect(element.shadowRoot.querySelector('c-logger-page-section')).toBeNull();
expect(element.shadowRoot.querySelector('c-logger-code-viewer')).toBeNull();
});

it('should render "No source snippet available" when the snippet field is empty', async () => {
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
element.sourceMetadata = 'Exception';
element.recordId = 'test-log-entry-id';

document.body.appendChild(element);
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: null }));
await flushPromises();

expect(element.shadowRoot.querySelector('lightning-spinner')).toBeNull();
expect(element.shadowRoot.querySelector('c-logger-page-section')).toBeNull();
expect(element.shadowRoot.querySelector('c-logger-code-viewer')).toBeNull();
expect(element.shadowRoot.textContent).toContain('No source snippet available');
});

it('should produce a .trigger title for ApexTrigger metadata', async () => {
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
element.sourceMetadata = 'Exception';
element.recordId = 'test-log-entry-id';

document.body.appendChild(element);
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexTrigger', snippet: buildSnippet() }));
await flushPromises();

const codeViewerTitle = element.shadowRoot.querySelector('c-logger-code-viewer span[slot="title"]');
expect(codeViewerTitle).toBeTruthy();
expect(codeViewerTitle.textContent).toBe('SomeApexClass.trigger - 65.0');
});

it('should hide the "View Full Source" button when there is no full source metadata', async () => {
// getMetadata resolves with an empty object → hasFullSourceMetadata is false.
getMetadata.mockResolvedValue({});
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
element.sourceMetadata = 'Exception';
element.recordId = 'test-log-entry-id';

document.body.appendChild(element);
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
await flushPromises();

const actionsSlotButtons = element.shadowRoot.querySelectorAll('c-logger-code-viewer span[slot="actions"] lightning-button');
expect(actionsSlotButtons.length).toBe(0);
});

it('should open the full-source modal with success notification when code is unmodified', async () => {
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
element.sourceMetadata = 'Exception';
element.recordId = 'test-log-entry-id';
document.body.appendChild(element);
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
await flushPromises();

const viewFullSourceButton = element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button');
viewFullSourceButton.click();
await flushPromises();

const modalSection = element.shadowRoot.querySelector('section.slds-modal');
expect(modalSection).toBeTruthy();
const modalTitle = element.shadowRoot.querySelector('section.slds-modal h2.slds-text-heading_medium');
expect(modalTitle.textContent).toBe('Full Source: SomeApexClass.cls - 65.0');
const notification = element.shadowRoot.querySelector('section.slds-modal div[role="alert"]');
expect(notification.classList.contains('slds-theme_success')).toBe(true);
expect(notification.classList.contains('slds-theme_warning')).toBe(false);
const notificationIcon = notification.querySelector('lightning-icon');
expect(notificationIcon.iconName).toBe('utility:success');
const notificationMessage = notification.querySelector('h2');
expect(notificationMessage.textContent).toBe('This Apex code has not been modified since this log entry was generated.');
const modalCodeViewer = element.shadowRoot.querySelector('section.slds-modal c-logger-code-viewer');
expect(modalCodeViewer).toBeTruthy();
expect(modalCodeViewer.code).toBe('full code here');
});

it('should open the full-source modal with warning notification when code has been modified', async () => {
getMetadata.mockResolvedValue({ Code: 'modified code', HasCodeBeenModified: true });
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
element.sourceMetadata = 'Exception';
element.recordId = 'test-log-entry-id';
document.body.appendChild(element);
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
await flushPromises();

element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button').click();
await flushPromises();

const notification = element.shadowRoot.querySelector('section.slds-modal div[role="alert"]');
expect(notification.classList.contains('slds-theme_success')).toBe(false);
expect(notification.classList.contains('slds-theme_warning')).toBe(true);
const notificationIcon = notification.querySelector('lightning-icon');
expect(notificationIcon.iconName).toBe('utility:warning');
const notificationMessage = notification.querySelector('h2');
expect(notificationMessage.textContent).toBe('This Apex code has been modified since this log entry was generated.');
});

it('should close the modal when the header close icon is clicked', async () => {
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
element.sourceMetadata = 'Exception';
element.recordId = 'test-log-entry-id';
document.body.appendChild(element);
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
await flushPromises();

element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button').click();
await flushPromises();
expect(element.shadowRoot.querySelector('section.slds-modal')).toBeTruthy();

element.shadowRoot.querySelector('section.slds-modal button.slds-modal__close').click();
await flushPromises();
expect(element.shadowRoot.querySelector('section.slds-modal')).toBeNull();
});

it('should close the modal when the footer Close button is clicked', async () => {
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
element.sourceMetadata = 'Exception';
element.recordId = 'test-log-entry-id';
document.body.appendChild(element);
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
await flushPromises();

element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button').click();
await flushPromises();

element.shadowRoot.querySelector('lightning-button[data-id="close-btn"]').click();
await flushPromises();
expect(element.shadowRoot.querySelector('section.slds-modal')).toBeNull();
});

it('should close the modal when the Escape key is pressed', async () => {
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
element.sourceMetadata = 'Exception';
element.recordId = 'test-log-entry-id';
document.body.appendChild(element);
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
await flushPromises();

element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button').click();
await flushPromises();

const modalSection = element.shadowRoot.querySelector('section.slds-modal');
modalSection.dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape', bubbles: true }));
await flushPromises();

expect(element.shadowRoot.querySelector('section.slds-modal')).toBeNull();
});

it('should leave the modal open when a non-Escape key is pressed', async () => {
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
element.sourceMetadata = 'Exception';
element.recordId = 'test-log-entry-id';
document.body.appendChild(element);
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
await flushPromises();
element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button').click();
await flushPromises();

const modalSection = element.shadowRoot.querySelector('section.slds-modal');
modalSection.dispatchEvent(new KeyboardEvent('keydown', { code: 'Enter', bubbles: true }));
await flushPromises();

expect(element.shadowRoot.querySelector('section.slds-modal')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
**********************************************************************************************-->

<template>
<template if:false={hasLoaded}>
<template if:false={isLoaded}>
<div class="slds-is-relative" style="min-height: 6em">
<lightning-spinner></lightning-spinner>
</div>
</template>
<template if:true={hasLoaded}>
<template if:true={isLoaded}>
<template if:false={sourceSnippet}>No source snippet available</template>
</template>
<template if:true={sourceSnippet}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default class LogEntryMetadataViewer extends LightningElement {
@api sourceMetadata;

objectApiName = LOG_ENTRY_OBJECT;
hasLoaded = false;
isLoaded = false;
sourceSnippet;

showFullSourceMetadataModal = false;
Expand All @@ -59,7 +59,7 @@ export default class LogEntryMetadataViewer extends LightningElement {

get fullSourceModalNotificationClasses() {
const classNames = ['slds-notify', 'slds-notify_alert'];
classNames.push(this._logEntryMetadata?.HasCodeBeenModified ? 'slds-alert_warning' : 'slds-alert_offline');
classNames.push(this._logEntryMetadata?.HasCodeBeenModified ? 'slds-theme_warning' : 'slds-theme_success');
return classNames.join(' ');
}

Expand All @@ -85,6 +85,7 @@ export default class LogEntryMetadataViewer extends LightningElement {
const sourceSnippetJson = getFieldValue(this._logEntry, sourceSnippetField);

if (!sourceSnippetJson) {
this.isLoaded = true;
return;
}

Expand All @@ -94,13 +95,13 @@ export default class LogEntryMetadataViewer extends LightningElement {
const sourceApiName = getFieldValue(this._logEntry, sourceApiNameField);
const sourceApiVersion = getFieldValue(this._logEntry, sourceApiVersionField);
const sourceName = `${sourceApiName}.${sourceExtension} - ${sourceApiVersion}`;
this.sourceSnippet = { ...JSON.parse(sourceSnippetJson), ...{ Title: sourceName } };

this.hasLoaded = true;
this.sourceSnippet = { ...JSON.parse(sourceSnippetJson), ...{ Title: sourceName } };
this._logEntryMetadata = await getMetadata({
recordId: this.recordId,
sourceMetadata: this.sourceMetadata
});
this.isLoaded = true;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jest.mock(
return {
loadScript() {
return new Promise((resolve, _) => {
global.Prism = require('../../../staticresources/LoggerResources/prism.js');
global.Prism = require('../../../staticresources/LoggerResources/Prism/prism.min.js');
resolve();
});
},
Expand Down
Loading