How to Customize Logging#

You want HAAPI logs in your existing observability backend (crash reporter, log aggregator) instead of (or in addition to) the system console. HaapiLogger exposes a unified LogSink mechanism that delivers every framework log record — Driver, SDK, and UI Layer — to your handler.

For the concept (severity ordering, follow-up tags, masking semantics), see Logging and Observability. For per-layer tag inventories, see Driver, SDK, UI Layer logging pages.

Prerequisites#

  • A working HAAPI integration.
  • An observability backend with a Kotlin / Swift SDK that accepts log messages (Crashlytics, Sentry, Datadog, internal logger — any sink with a “log this string” API).

Step-by-Step#

1. Configure the logger at app startup#

Set the level, opt in to the tags you want to see, and confirm sensitive-value masking is on.

SDK Layer#

// In AppDelegate.application(_:didFinishLaunchingWithOptions:)
HaapiLogger.followUpTags = DriverFollowUpTag.allCases + SdkFollowUpTag.allCases
HaapiLogger.setLogType(LogType.info)
// isSensitiveValueMasked defaults to true; leave it that way in production.

UI Layer#

Add the UI follow-up tags on top of the Driver + SDK sets:

HaapiLogger.followUpTags = DriverFollowUpTag.allCases
    + SdkFollowUpTag.allCases
    + UIKitFollowUpTag.allCases
HaapiLogger.setLogType(LogType.info)

Without UIKitFollowUpTag.allCases, every UI-layer record is silently suppressed — a common cause of “the SDK looks fine but the screens log nothing”.

SDK Layer#

// In Application.onCreate
HaapiLogger.enabled = true
HaapiLogger.setLevel(HaapiLogger.LogLevel.INFO)
// isSensitiveValueMasked defaults to true; leave it that way in production.

UI Layer#

Same call — the UI-layer records flow through alongside Driver and SDK once the logger is enabled. Filter at the sink (next step) or by LogCat tag prefix (logcat -s 'HAAPI_UI_*:V').

React Native has only an SDK Layer surface. getRNLogger is a singleton factory — call it once at startup with the configuration; subsequent calls return the same instance. The configuration is forwarded to the native iOS and Android loggers atomically:

import { getRNLogger, RNLogLevel, initializeForHaapi } from 'identityserver.haapi.reactnative.sdk'

const logger = getRNLogger({
    logLevel: RNLogLevel.Info,           // Info | Debug | Trace | Warn | Error
    isSensitiveValueMasked: true,         // keep true outside local development
})

const accessor = await initializeForHaapi(haapiConfig, logger)

Records emitted by both native layers (iOS and Android) reach the JS side through a single event channel. Tag prefixes (HAAPI_DRIVER_*, HAAPI_SDK_*) survive the bridge — filter them in your sink, not at the native side.

2. Implement and register a LogSink#

The sink receives every log record with its level, follow-up tag, message, file, and line. Keep the implementation fast — sinks fire on the calling thread.

final class CrashReporterLogSink: LogSink {
    func log(
        level: LogType,
        tag: FollowUpTag,
        message: String,
        file: String,
        line: Int
    ) {
        // Forward to your crash reporter
        MyCrashReporter.log(
            level: level.rawValue,
            tag: "\(tag)",
            message: message,
            file: file,
            line: line
        )
    }
}

HaapiLogger.add(sink: CrashReporterLogSink())
class CrashReporterLogSink : LogSink {
    override fun log(
        level: HaapiLogger.LogLevel,
        tag: String,
        message: String,
        file: String,
        line: Int
    ) {
        // Forward to your crash reporter
        MyCrashReporter.log(level.name, tag, message, file, line)
    }
}

HaapiLogger.addSink(CrashReporterLogSink())
import type { RNLogEvent } from 'identityserver.haapi.reactnative.sdk'

const sinkHandle = logger.addSink((event: RNLogEvent) => {
    // Forward to your crash reporter / analytics pipeline.
    MyCrashReporter.log(event.level, event.tag, event.message, event.file, event.line)
    // Keep this fast — sinks fire on the JS thread synchronously.
})

// Later: sinkHandle.remove() to detach the sink.

RN sinks run on the JS thread (unlike native sinks, which run on the calling native thread). For heavy work — network I/O, large serialisation — post to a worker or queue and return immediately.

3. (Optional) Filter by tag prefix inside the sink#

If you only want SDK Layer records in your crash reporter, filter inside log(...):

// Android example — same idea applies on iOS
if (!tag.startsWith("HAAPI_SDK")) return

Verify It Works#

  • Run the app and complete a HAAPI flow.
  • Your crash-reporter dashboard should show records tagged HAAPI_DRIVER_*, HAAPI_SDK_*, and (if at the UI Layer) HAAPI_UI_*.
  • Sensitive values (tokens, nonces) should appear as *****… in the message strings, confirming masking is on.

Pitfalls#

  • isSensitiveValueMasked = false + remote sink. Disabling masking and shipping logs to a remote backend leaks access tokens, refresh tokens, DPoP keys, and dpop-nonce headers off-device. Treat this combination as a hard “never” — keep masking on outside of local debugging.
  • iOS followUpTags is an emission gate. iOS records are dropped unless their tag is in HaapiLogger.followUpTags, which defaults to [] (so nothing logs until you set it). Forgetting to add SdkFollowUpTag.allCases or UIKitFollowUpTag.allCases silently suppresses entire layers. Android has no tag-based gating — enabled and setLevel(...) are the only gates, and tags travel to the sink as labels. If you only want a subset of records on Android, filter inside your LogSink.log(...) by inspecting the tag.
  • Slow sink. The sink fires on the calling thread. A sink that blocks on network I/O or grabs a lock can stall flow progression. Buffer records and ship asynchronously if your backend isn’t local.
  • Forgotten release-mode log level. Android’s setLevel defaults to VERBOSE in DEBUG builds and ERROR in release. iOS does not auto-switch. Pick the level explicitly in production builds to avoid surprises.

Was this helpful?