Logging

The Descope mobile SDKs can emit log messages to help you diagnose authentication, session, and flow issues during development.

Logging is disabled by default. No output is produced until you explicitly set a logger on the SDK configuration.

Built-in Loggers

The Swift, Kotlin, and Flutter SDKs each ship three built-in loggers. They differ in how much they print, and in whether they print unsafe runtime values — full network request and response payloads, tokens, secrets, and personal information.

LoggerPrintsUnsafe values
basicLoggerErrors and infoNever
debugLoggerErrors, info and debugOnly when the app is running in a debug context
unsafeLoggerErrors, info and debugAlways

debugLogger is the right choice in most cases. You can add it while diagnosing an issue, and it will not leak sensitive data if you forget to remove it before shipping a build to the App Store or Play Store.

The debug context that debugLogger detects differs between platforms. On Swift it means a debugger is attached to the process, so the app was launched from Xcode. On Kotlin it means the application is marked debuggable, whether or not Android Studio is attached. On Flutter it means the app was compiled in debug mode, so unsafe values are never printed in profile or release builds.

React Native does not use these built-in loggers - see the React Native tab under Enabling a Logger below.

Even with unsafe values off, error logs stay actionable - the Descope error code and API route are part of the message itself. For example, a failed OTP verification on Android prints a log line like this:

[DescopeAndroid] Network call to auth/otp/verify/email failed with E061102 server error

Each SDK prefixes its log lines with its own SDK name: [DescopeKit] on Swift, [DescopeAndroid] on Kotlin, and [DescopeFlutter] on Flutter.

Log Levels

Every log message carries one of three severities:

LevelUsed for
errorFailures that stopped an operation, such as a network error or a timed-out enchanted link
infoNormal lifecycle milestones, such as a network call starting or finishing
debugFine-grained detail useful when tracing a problem, such as individual polling attempts

A logger prints messages at its configured level and everything more severe. A logger set to info prints error and info messages but drops debug ones, which is exactly what basicLogger does.

How the level is represented differs by platform:

DescopeLogger.Level is an enum.

DescopeLogger.Level.error
DescopeLogger.Level.info
DescopeLogger.Level.debug

DescopeLogger.Level is an enum.

DescopeLogger.Level.Error
DescopeLogger.Level.Info
DescopeLogger.Level.Debug

Flutter has no enum. The levels are integer constants on DescopeLogger, ordered from most to least severe.

DescopeLogger.error // 0
DescopeLogger.info  // 1
DescopeLogger.debug // 2

This matters when you set the level property yourself, since it takes an int rather than an enum value.

React Native has no level type. Your logger object exposes a separate method per severity, and the SDK calls the one that matches the message. See the React Native tab under Enabling a Logger.

Enabling a Logger

Descope.setup(projectId: "__ProjectID__") { config in
    config.logger = .debugLogger
}
Descope.setup(this, projectId = "__ProjectID__") {
    logger = DescopeLogger.debugLogger
}
Descope.setup('__ProjectID__', (config) {
  config.logger = DescopeLogger.debugLogger;
});

On Flutter, log messages from the underlying native iOS and Android SDKs are forwarded to your logger automatically once one is set, so native flow execution appears alongside your Dart logs.

Your logger's unsafe setting is passed through to the native layer, so unsafeLogger also captures native payloads.

The React Native SDK takes a logger object rather than one of the built-in loggers above. Pass it to AuthProvider:

const logger = {
  log: (message) => console.log(message),
  debug: (message) => console.debug(message),
  warn: (message) => console.warn(message),
  error: (message) => console.error(message),
}

<AuthProvider projectId="__ProjectID__" logger={logger}>
  <App />
</AuthProvider>

During development you can pass console directly, or point the methods at a monitoring service instead.

The bridge forwards log messages from the native iOS and Android SDKs to this logger, so native flow execution shows up alongside your JavaScript logs. That helps when a flow loads but never completes. Native info messages arrive as log.

The bridge always runs with unsafe logging disabled, so it never prints native payloads or tokens. React Native has no equivalent of unsafeLogger.

What Gets Logged

Once a logger is set, the SDK reports on the operations below. Not every area is covered on every platform, because Flutter and React Native run parts of the authentication natively and those messages arrive through the native log bridge rather than from the Dart or JavaScript layer.

AreaWhat you'll see
Network requestsEvery call starting and finishing, plus server, HTTP and network failures with their codes
Session refreshRefreshes triggered by an expiring token, skipped refreshes, and periodic refresh outcomes
Flow executionFlow start, ready, success and failure, resume URLs, and native OAuth or web authentication
Enchanted link pollingPolling start, each wait, success, and timeout
Persisted session lookupWhether a stored session was found at startup

Custom Loggers

The built-in loggers print to the console, which is fine while you are working locally. If you want to route Descope log messages into your own logging framework or a third party monitoring service, provide your own logger instead.

On Swift, Kotlin, and Flutter you do this by subclassing DescopeLogger and overriding its output method. The base class handles level filtering and unsafe-value filtering for you, then hands the surviving messages to output.

You also choose the level and unsafe behavior yourself by passing them to the initializer, rather than inheriting the fixed combination that a built-in logger uses.

Descope.setup(projectId: "__ProjectID__") { config in
    config.logger = RemoteDescopeLogger()
}

// elsewhere

class RemoteDescopeLogger: DescopeLogger {
    init() {
        super.init(level: .info, unsafe: false)
    }

    override func output(level: Level, message: String, unsafe values: [Any]) {
        RemoteLogger.sendLog("Descope: \(message)")
    }
}
Descope.setup(this, projectId = "__ProjectID__") {
    logger = RemoteDescopeLogger()
}

// elsewhere

class RemoteDescopeLogger : DescopeLogger(level = Level.Info, unsafe = false) {
    override fun output(level: Level, message: String, values: List<Any>) {
        RemoteLogger.sendLog("Descope: $message")
    }
}
Descope.setup('__ProjectID__', (config) {
  config.logger = RemoteDescopeLogger();
});

// elsewhere

class RemoteDescopeLogger extends DescopeLogger {
  RemoteDescopeLogger() : super(level: DescopeLogger.info, unsafe: false);

  @override
  void output({required int level, required String message, required List<dynamic> values}) {
    RemoteLogger.sendLog('Descope: $message');
  }
}

To override how the SDK performs HTTP requests rather than how it logs them, see Network Client.

Was this helpful?

On this page