Skip to main contentMAF Configuration Practices

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

MethodLevelVisible by defaultWhen to use
log.e(TAG, msg, err)ERROR✅ AlwaysUnrecoverable failure — the operation cannot continue. User will see an error or blank screen.
log.w(TAG, msg)WARN✅ AlwaysUnexpected 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✅ AlwaysKey lifecycle milestone — login started, sync complete, user switched. One line per meaningful event.
log.d(TAG, msg)DEBUG❌ Opt-in onlyDetailed diagnostic — which query ran, how long it took, what data was received. Enable via Navigator General Settings.
log.t(TAG, msg)TRACE❌ Opt-in onlyPer-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, or log.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 repeatedly
log.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 lost
try {
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 continues
if (!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 noise
log.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 only
log.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 only
records.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

  1. Declare TAG as a module-level constant — never inline a string in a log call.
  2. Format: framework + '-' + ClassName for framework code, appId + '-' + ClassName for app code.
  3. Use the existing TAG constants from adjacent files — do not invent new formats.
// ✅ correct
import {framework} from '../../Constants';
const TAG = framework + '-DisconnectedDataAdapter';
log.e(TAG, 'Load Error', e);
// ❌ wrong — inline string, no consistency
log.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 messages
log.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.stringify at INFO or above — this serialises entire datasource state and floods the log
  • Do not use console.log — use log.d or log.t instead; console.log bypasses 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 token
log.d(TAG, 'access_token', accessToken);
// ✅ correct — log presence only
log.d(TAG, 'access_token obtained');
// ❌ wrong — large object serialisation at INFO
log.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 flowLevelMessage pattern
Login type determinedlog.i'loginType', loginType value
Server auth resultlog.i'isServerAuthenticated', boolean
Server auth failed → return falselog.w'login: server auth failed for loginType X — returning false'
Token refresh succeededlog.i'tokenValid, no need to authenticate again.'
Token refresh failed — degradedlog.w'generateToken failed — will fall through to manual login'
Token response missing fieldslog.w'generateToken: token response missing access_token or refresh_token'
OIDC callback receivedlog.i'OIDC callback received, access_token obtained'
OIDC callback missing codelog.e'oidc callback has no parameter code'
Local DB activation failedlog.e'Connect login to locally', error
Biometric failedlog.e'failed to add biometric', error

Sync lifecycle — required log statements

Point in flowLevelMessage pattern
Download started (app name, datasource)log.iFrom DataDownloadEventListener progress event
Download complete (record count, elapsed)log.i'sync complete — N records in Xms'
Page size > 50log.w'Page size N is greater than 50. Consider reducing...' (use log.wOnce)
Schema not found for objectlog.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 failure
if (!isServerAuthenticated && loginType === LOGIN_TYPE_FIRSTUSER) {
return false;
}
// ✅ correct
if (!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 log
records.forEach(record => {
log.i(TAG, 'processing record', record.href);
});
// ✅ correct
records.forEach(record => {
log.t(TAG, 'processing record', record.href);
});

PR checklist

Before raising a PR that includes log changes, verify:

  • Every new catch block has at least a log.w or log.e
  • No silent return false without a preceding log.w
  • No console.log calls — use log.d or log.t
  • No sensitive data (tokens, passwords, PII) in any log message
  • TAGs declared as module-level constants
  • High-volume loop/callback logs use log.t not log.i/log.d
  • One-shot warnings use log.wOnce / log.eOnce
Page last updated: 01 September 2026