🔹 Why this matters
Good logging makes bugs reproducible, fixable, and rare. In NetSuite, you’ll triage via Execution Logs (server-side) and the browser console (client-side). This guide shows:
- What to log (and what not to)
- How to structure logs for fast filtering
- Patterns for server, client, Scheduled, and Map/Reduce scripts
- Safe logging (no secrets) and governance-aware timing
🔹 The logging toolbox (quick reference)
Server-side (UE, Scheduled, M/R, Suitelet, RESTlet)
log.debug(title, details)
– verbose info (default filtered in Prod)log.audit(title, details)
– noteworthy events/state changeslog.error(title, details)
– failures you must seelog.emergency(title, details)
– critical outages (rare)
Tips
details
can be objects → wrap withJSON.stringify(details)
when needed.- Avoid dumping entire records or big payloads; redact and truncate.
Client-side (Client Script)
console.log/info/warn/error(...)
– view in browser devtoolsdebugger;
– sets a breakpoint if devtools are open- Avoid
alert()
except for UX prompts
🔹 Make logs filterable: include a request/run “Correlation ID”
Add a lightweight correlation ID per execution. Put it in every log so you can grep a failing run.
function makeCorrelationId(prefix='RUN') {
// e.g., RUN-1732549012345-7f3a
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2,6)}`;
}
Use it in every log line:
const cid = makeCorrelationId('CUSTSYNC');
log.debug('Start', JSON.stringify({ cid, deployment: runtime.getCurrentScript().deploymentId }));
🔹 Safe logging helper (redact & truncate)
Never log secrets or PII in plain text.
function redact(value) {
if (!value) return value;
const s = String(value);
if (/^Bearer\s+/i.test(s)) return 'Bearer ***';
if (s.length > 1200) return s.slice(0, 1200) + '…[truncated]';
return s.replace(/([A-Za-z0-9]{4})([A-Za-z0-9]+)([A-Za-z0-9]{4})/g, '$1***$3'); // coarse mask
}
function safe(obj) {
try { return JSON.stringify(obj, (k, v) => (k.match(/key|secret|token|password/i) ? '***' : v)); }
catch (_) { return '[unserializable]'; }
}
🔹 Pattern: Try–catch with context-rich logs
Wrap critical sections; include record IDs, counts, and the correlation ID.
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/runtime'], (runtime) => {
const beforeSubmit = (context) => {
const cid = makeCorrelationId('UE');
try {
const rec = context.newRecord;
log.audit('UE:beforeSubmit:start', safe({ cid, type: rec.type, id: rec.id, mode: context.type }));
// … your logic …
log.debug('UE:beforeSubmit:end', safe({ cid }));
} catch (e) {
log.error('UE:beforeSubmit:error', redact(e && (e.stack || e.message || e)));
throw e; // block save when validation fails
}
};
return { beforeSubmit };
});
🔹 Toggle verbosity via Deployment “Log Level”
In the Script Deployment record:
- Dev/Sandbox: Debug
- Production: Audit (bump to Error for noisy scripts)
You can also add a script parameter custscript_log_level
and gate your log.debug
calls.
function shouldDebug(runtime) {
const lvl = (runtime.getCurrentScript().getParameter({ name: 'custscript_log_level' }) || '').toLowerCase();
return lvl === 'debug';
}
// usage: if (shouldDebug(runtime)) log.debug('Detail', safe({...}));
🔹 Time and governance checkpoints
Measure performance and avoid usage surprises.
define(['N/runtime'], (runtime) => {
function timer() { const t0 = Date.now(); return (label) => log.audit('TIMER', `${label} ms=${Date.now()-t0}`); }
const execute = () => {
const tic = timer();
const script = runtime.getCurrentScript();
// … step 1 …
tic('after step 1');
// log remaining governance units
log.debug('Gov', `remaining=${script.getRemainingUsage()}`);
// … step 2 …
tic('after step 2');
};
return { execute };
});
🔹 Map/Reduce: summarize ALL the things
Use summarize
to dump errors and metrics in one place.
const summarize = (summary) => {
try {
log.audit('MR:summary', safe({ usage: summary.usage, yields: summary.yields }));
if (summary.inputSummary.error) {
log.error('MR:input:error', redact(summary.inputSummary.error));
}
summary.mapSummary.errors.iterator().each((key, err) => {
log.error('MR:map:error', safe({ key, err: redact(err) }));
return true;
});
summary.reduceSummary.errors.iterator().each((key, err) => {
log.error('MR:reduce:error', safe({ key, err: redact(err) }));
return true;
});
} catch (e) {
log.error('MR:summary:fail', redact(e && (e.stack || e.message || e)));
}
};
Tip: Also count processed records and successes vs failures; include in audit
.
🔹 Scheduled scripts: graceful stop + reschedule logs
When usage is low, log why you stop and reschedule.
define(['N/runtime','N/task'], (runtime, task) => {
const execute = () => {
const cid = makeCorrelationId('SCH');
try {
const rem = runtime.getCurrentScript().getRemainingUsage();
if (rem < 150) {
log.audit('SCH:reschedule', safe({ cid, reason: 'low usage', remaining: rem }));
task.create({
taskType: task.TaskType.SCHEDULED_SCRIPT,
scriptId: runtime.getCurrentScript().id,
deploymentId: runtime.getCurrentScript().deploymentId
}).submit();
return;
}
// … continue work …
log.debug('SCH:done', safe({ cid }));
} catch (e) {
log.error('SCH:error', redact(e && (e.stack || e.message || e)));
}
};
return { execute };
});
🔹 Client Script debugging (browser)
- Open browser devtools (F12) on the NetSuite page.
- Use
console.log()
with clear prefixes and IDs. - Use
debugger;
to break:
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
*/
define(['N/currentRecord'], (currentRecord) => {
const pageInit = () => {
console.log('[CS] pageInit start');
// debugger; // uncomment during development
const rec = currentRecord.get();
console.log('[CS] record type:', rec.type);
};
return { pageInit };
});
Note: log.debug
does not exist in Client Scripts.
🔹 Persisting diagnostic breadcrumbs (optional)
For stubborn bugs, persist breadcrumbs to a custom record or a small File Cabinet text file—careful with volume and secrets.
// Write a small line to a text file for post-mortem (rotate or cap size in prod)
define(['N/file'], (file) => {
function appendLine(name, text, folderId) {
try {
const f = file.create({ name, fileType: file.Type.PLAINTEXT, contents: text + '\n' });
if (folderId) f.folder = folderId;
return f.save();
} catch (e) { log.error('appendLine error', e.message); }
}
});
🔹 What NOT to log
- Secrets: tokens, passwords, full Authorization headers
- PII: full emails, phone numbers; mask if needed
- Huge payloads: truncate or summarize counts/keys
- Whole records: log only fields you need
🔹 Triage checklist (production issues)
- Get Correlation ID (or timestamp + deployment).
- Check Script Execution Logs (Script Deployment → Execution Log).
- Search for
error
/emergency
first, thenaudit
. - Reproduce in Sandbox with Debug log level.
- Add timers around suspected hotspots.
- If data-dependent, log record IDs and search filters used.
- For external calls, log status code, duration, and a redacted body sample.
🔹 Copy-paste helpers (drop in /SuiteScripts/lib/logging.js
)
export function makeCorrelationId(prefix='RUN') {
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2,6)}`;
}
export function safe(obj) {
try { return JSON.stringify(obj, (k, v) => (k.match(/key|secret|token|password/i) ? '***' : v)); }
catch (_) { return '[unserializable]'; }
}
export function redact(value) {
if (!value) return value;
const s = String(value);
if (/^Bearer\s+/i.test(s)) return 'Bearer ***';
if (s.length > 1200) return s.slice(0, 1200) + '…[truncated]';
return s.replace(/([A-Za-z0-9]{4})([A-Za-z0-9]+)([A-Za-z0-9]{4})/g, '$1***$3');
}
✅ Key Takeaway
- Structure logs with a correlation ID and consistent prefixes.
- Log safely (redact, truncate), and at the right level (debug/audit/error).
- Measure time and governance, and always summarize Map/Reduce errors.
- For client issues, use the browser console and
debugger;
.