Logging Good Practices
Overview
Maximo Mobile uses the Graphite Log framework (log.e / log.w / log.i / log.d / log.t) for all JS-layer logging. Good log statements are the fastest path to resolving customer issues. Poor ones either flood support logs with noise or leave failures completely silent.
This guide covers:
- Which log level to use in each situation
- How to write a useful log message
- TAG naming rules
- What not to log (security and performance)
- Examples of good and bad log statements
Log levels
| Method | Level | Visible by default | When to use |
|---|---|---|---|
log.e(TAG, msg, err) | ERROR | ✅ Always | Unrecoverable failure — the operation cannot continue. User will see an error or blank screen. |
log.w(TAG, msg) | WARN | ✅ Always | Unexpected state that the app can recover from, or a degraded code path. Something is wrong but the user can still proceed. |
log.i(TAG, msg) | INFO | ✅ Always | Key lifecycle milestone — login started, sync complete, user switched. One line per meaningful event. |
log.d(TAG, msg) | DEBUG | ❌ Opt-in only | Detailed diagnostic — which query ran, how long it took, what data was received. Enable via Navigator General Settings. |
log.t(TAG, msg) | TRACE | ❌ Opt-in only | Per-iteration noise — loop bodies, event callbacks, crypto internals. Never visible in a production log. |
Production default is ERROR only. When a customer shares a log, you will see ERROR and WARN lines. INFO is also always visible. If you want a statement to appear in a default support log, it must be
log.e,log.w, orlog.i.
One-shot variants
Use log.eOnce, log.wOnce, log.iOnce to suppress duplicate messages within a session. Prefix the TAG with ONCE- automatically.
// fires once per session even if called repeatedlylog.wOnce(TAG, 'Page size exceeds 50 — consider reducing for offline sync performance');
Choosing the right level
Use log.e when
- An exception is caught and the operation cannot complete
- A required configuration key is missing and the feature will not work
- An OIDC/auth callback returns an unexpected response
- A SQLite statement fails and data was not saved
// ✅ correct — save failed, data losttry {await datasource.save();} catch (err) {log.e(TAG, 'save failed for workorder ' + wonum, err);throw err;}
Use log.w when
- A fallback path was taken (e.g. silent token refresh failed, fell through to manual login)
- A configuration value was missing but a default was applied
- A non-critical resource (image, attachment) could not be loaded
- A slow request completed but exceeded the performance threshold
- A retry is about to happen
// ✅ correct — degraded path, app continuesif (!tokenValid) {log.w(TAG, 'generateToken failed — falling back to manual login');}
Use log.i when
- A user-visible lifecycle milestone completes: login started, login succeeded, sync complete, logout
- The auth path selected (loginType, isServerAuthenticated) — essential for support
- A long-running operation starts or finishes (data download, schema load)
- App version or configuration is read on startup
// ✅ correct — production-visible, one line, no noiselog.i(TAG, 'loginType', loginType);log.i(TAG, 'isServerAuthenticated', isServerAuthenticated);
Use log.d when
- Logging the parameters of a query or request
- Timing a sub-operation (SQL query time, fetch time)
- Logging the count of records received per page
// ✅ correct — debug onlylog.d(TAG, 'performLoad query:', JSON.stringify(query));log.d(TAG, 'page ' + pageno + ' received ' + records.length + ' records in ' + timeDiff + 'ms');
Use log.t when
- Inside a loop body (per-record processing)
- In Cordova event callbacks (loadstop, loadstart, exit)
- In cryptographic helper functions
- Any message that fires more than once per user action
// ✅ correct — per-iteration, trace onlyrecords.forEach(record => {log.t(TAG, 'processing record', record.href);});
TAG naming
Every log call must include a TAG. TAGs identify the module or class emitting the log and are the primary filter in Kibana, Splunk, and the Navigator Log Data page.
Rules
- Declare TAG as a module-level constant — never inline a string in a log call.
- Format:
framework + '-' + ClassNamefor framework code,appId + '-' + ClassNamefor app code. - Use the existing TAG constants from adjacent files — do not invent new formats.
// ✅ correctimport {framework} from '../../Constants';const TAG = framework + '-DisconnectedDataAdapter';log.e(TAG, 'Load Error', e);
// ❌ wrong — inline string, no consistencylog.e('MyModule', 'something failed');
Proposed renames
Some existing TAGs are cryptic (MaximoMobile-FINDADAPTER, MaximoMobile-BULKDOCS). A rename proposal is tracked in maximo-disconnected-enablement/docs/log-tag-rename-proposals.md. When adding new TAGs, use the proposed names from that document.
Message format
Do
- Start with a verb —
'Load failed','Token refresh succeeded','Schema not found for' - Include the key identifier — record href, datasource name, object structure, username (hashed/partial only)
- Include timing when relevant —
'sync completed in ' + elapsed + 'ms' - Pass the error object as the last argument —
log.e(TAG, 'Save failed', err)— the framework serialises it
// ✅ good messageslog.e(TAG, 'Save failed for datasource ' + datasourceId, err);log.w(TAG, 'login: server auth failed for loginType ' + loginType + ' — returning false');log.i(TAG, 'sync complete — ' + records + ' records in ' + elapsed + 'ms');
Do not
- Do not log raw sensitive data — passwords, access tokens, refresh tokens, full cookie values, PII
- Do not log large objects with
JSON.stringifyat INFO or above — this serialises entire datasource state and floods the log - Do not use
console.log— uselog.dorlog.tinstead;console.logbypasses the level filter and appears in every log - Do not leave unsubstituted format strings —
'Field %s not found'with no argument passed is a known existing issue; do not add new ones
// ❌ wrong — logs raw tokenlog.d(TAG, 'access_token', accessToken);// ✅ correct — log presence onlylog.d(TAG, 'access_token obtained');// ❌ wrong — large object serialisation at INFOlog.i(TAG, 'datasource state:', JSON.stringify(datasource));
Auth lifecycle — required log statements
Every auth entry point must emit these statements at minimum. This is the most common source of silent customer failures.
| Point in flow | Level | Message pattern |
|---|---|---|
| Login type determined | log.i | 'loginType', loginType value |
| Server auth result | log.i | 'isServerAuthenticated', boolean |
| Server auth failed → return false | log.w | 'login: server auth failed for loginType X — returning false' |
| Token refresh succeeded | log.i | 'tokenValid, no need to authenticate again.' |
| Token refresh failed — degraded | log.w | 'generateToken failed — will fall through to manual login' |
| Token response missing fields | log.w | 'generateToken: token response missing access_token or refresh_token' |
| OIDC callback received | log.i | 'OIDC callback received, access_token obtained' |
| OIDC callback missing code | log.e | 'oidc callback has no parameter code' |
| Local DB activation failed | log.e | 'Connect login to locally', error |
| Biometric failed | log.e | 'failed to add biometric', error |
Sync lifecycle — required log statements
| Point in flow | Level | Message pattern |
|---|---|---|
| Download started (app name, datasource) | log.i | From DataDownloadEventListener progress event |
| Download complete (record count, elapsed) | log.i | 'sync complete — N records in Xms' |
| Page size > 50 | log.w | 'Page size N is greater than 50. Consider reducing...' (use log.wOnce) |
| Schema not found for object | log.e | 'Schema not found in master schema for object: X' |
| Fetch failed (HTTP error) | log.e | '[datasourceId]: Datasource fetch failed' |
| Slow request (> threshold) | log.w | 'Request for X taking N miliseconds' (emitted by framework automatically) |
Common anti-patterns
Silent return false
The most common cause of unexplained blank screens. Always log before returning false from an auth or init method.
// ❌ wrong — completely silent failureif (!isServerAuthenticated && loginType === LOGIN_TYPE_FIRSTUSER) {return false;}// ✅ correctif (!isServerAuthenticated && loginType === LOGIN_TYPE_FIRSTUSER) {log.w(TAG, 'login: server auth failed for loginType ' + loginType + ' — returning false');return false;
Swallowed catch with wrong level
// ❌ wrong — network error during token refresh logged at INFO, missed by support} catch (err) {log.i(TAG, 'generateToken error', err);}// ✅ correct} catch (err) {log.w(TAG, 'generateToken failed — will fall through to manual login:', err);}
High-volume log at INFO
// ❌ wrong — fires 200 times per sync page, floods logrecords.forEach(record => {log.i(TAG, 'processing record', record.href);});// ✅ correctrecords.forEach(record => {log.t(TAG, 'processing record', record.href);});
PR checklist
Before raising a PR that includes log changes, verify:
- Every new
catchblock has at least alog.worlog.e - No silent
return falsewithout a precedinglog.w - No
console.logcalls — uselog.dorlog.t - No sensitive data (tokens, passwords, PII) in any log message
- TAGs declared as module-level constants
- High-volume loop/callback logs use
log.tnotlog.i/log.d - One-shot warnings use
log.wOnce/log.eOnce