SDK Features¶
Configuration¶
The Logger
needs to be started before it can be used. Note that the start
method only needs to be called once and will cause the Logger to retain this configuration until the end of the process. Subsequent calls to the start
method won't have any effect.
The minimum required configuration of the SDK is as follows:
import io.bitdrift.capture.Capture.Logger
import io.bitdrift.capture.providers.session.SessionStrategy
Logger.start(
apiKey = "<your-api-key>",
sessionStrategy = SessionStrategy.ActivityBased(),
)
Initialization¶
The Android SDK handles its initial configuration using Jetpack Startup1. It uses an Initializer
called ContextHolder
to get a reference to the application's Context
.
Initialization Dependency¶
If you wish to start the Logger
from inside your own Initializer
make sure to add it as a dependency:
import io.bitdrift.capture.ContextHolder
class AppExampleInitializer : Initializer<AppExampleDependency> {
override fun create(context: Context): AppExampleDependency {
// You can call Capture.Logger.start() here safely
return AppExampleDependency()
}
override fun dependencies(): List<Class<out Initializer<*>>> {
// Defines a dependency on ContextHolder so it can be
// initialized after bitdrift Capture is initialized.
return listOf(ContextHolder::class.java)
}
}
Manual Initialization¶
If you wish to avoid automatic initialization and want to manually call the bitdrift Capture initializer you can follow the steps below.
Add this to your AndroidManifest.xml configuration to disable automatic initialization:
<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge"
>
<!-- Disable automatic initialization of Bitdrift ContextHolder -->
<meta-data
android:name="io.bitdrift.capture.ContextHolder"
tools:node="remove"
/>
</provider>
</application>
Manually call the bitdrift initializer:
import io.bitdrift.capture.ContextHolder
// This needs to run before calling Logger.start()
AppInitializer.getInstance(applicationContext)
.initializeComponent(ContextHolder::class.java)
import io.bitdrift.capture.Capture.Logger;
import io.bitdrift.capture.providers.session.SessionStrategy;
Logger.start(
"<your-api-key>",
new SessionStrategy.ActivityBased()
);
Initialization¶
The Android SDK handles its initial configuration using Jetpack Startup1. It uses an Initializer
called ContextHolder
to get a reference to the application's Context
.
Initialization Dependency¶
If you wish to start the Logger
from inside your own Initializer
make sure to add it as a dependency:
import io.bitdrift.capture.ContextHolder;
class AppExampleInitializer implements Initializer<AppExampleDependency> {
@Override
public AppExampleDependency create(context: Context) {
// You can call Capture.Logger.start() here safely
return new AppExampleDependency();
}
@Override
public List<Class<Initializer<?>>> dependencies() {
// Defines a dependency on ContextHolder so it can be
// initialized after bitdrift Capture is initialized.
return Arrays.asList(ContextHolder.class);
}
}
Manual Initialization¶
If you wish to avoid automatic initialization and want to manually call the bitdrift Capture initializer you can follow the steps below.
Add this to your AndroidManifest.xml configuration to disable automatic initialization:
<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge"
>
<!-- Disable automatic initialization of Bitdrift ContextHolder -->
<meta-data
android:name="io.bitdrift.capture.ContextHolder"
tools:node="remove"
/>
</provider>
</application>
Manually call the bitdrift initializer:
import io.bitdrift.capture.ContextHolder;
// This needs to run before calling Logger.start()
AppInitializer.getInstance(context)
.initializeComponent(ContextHolder.class);
import Capture
Logger.start(
withAPIKey: "<your-api-key>",
sessionStrategy: .activityBased()
)
Info
It's recommended that you initialize the SDK as part of your application's application(_:willFinishLaunchingWithOptions:)
or application(_:didFinishLaunchingWithOptions:)
methods. This allows the SDK to observe system events such as didFinishLaunchingNotification
, which power some of the SDK's out-of-the-box events.
#import <Capture/Capture.h>
[CAPLogger
startWithAPIKey:@"your-api-key"
sessionStrategy: [CAPSessionStrategy activityBased]
];
Info
It's recommended that you initialize the SDK as part of your application's application(_:willFinishLaunchingWithOptions:)
or application(_:didFinishLaunchingWithOptions:)
methods. This allows the SDK to observe system events such as didFinishLaunchingNotification
, which power some of the SDK's out-of-the-box events.
When using the JavaScript-based initialization (e.g. using Expo), the SDK can be initialized as follows:
import { init, SessionStrategy } from '@bitdrift/react-native';
init("<your-api-key>", SessionStrategy.Activity);
When using the native initialization (e.g. ejected Expo, non-Expo React Native), the SDK can be initialized using the per platform instructions above.
import { init, SessionStrategy } from '@bitdrift/react-native';
init("<your-api-key>", SessionStrategy.Activity);
The SDK should be initialized in the main process.
See the Quickstart Guide section for information on how to obtain an API Key.
Session Management¶
Events collected by the SDK are annotated with a session identifier (ID). This session identifier is utilized to group events emitted by the SDK and can be used to retrieve events from a specific session.
The SDK manages the used session identifier using one of the two following strategies:
ActivityBased
(Kotlin) /activityBased
(Swift) /SessionStrategy.Activity
(JavaScript): A session strategy that generates a new session ID after a certain period of app inactivity. The inactivity duration is measured by the minutes elapsed since the last log. The session ID is persisted to disk and survives app restarts.Fixed
(Kotlin) /fixed
(Swift) /SessionStrategy.Fixed
: A session strategy that never expires the session ID but does not survive process restart.
The minimal session strategy setup looks as follows:
// `ActivityBased` session strategy
Logger.start(
// ...
sessionStrategy = SessionStrategy.ActivityBased()
)
// `Fixed` session strategy
Logger.start(
// ...
sessionStrategy = SessionStrategy.Fixed()
)
// `ActivityBased` session strategy
Logger.start(
// ...
new SessionStrategy.ActivityBased()
);
// `Fixed` session strategy
Logger.start(
// ...
new SessionStrategy.Fixed()
);
// `activityBased` session strategy
Logger.start(
// ...
sessionStrategy: .activityBased()
)
// `fixed` session strategy
Logger.start(
// ...
sessionStrategy: .fixed()
)
// `activityBased` session strategy
[CAPLogger
// ...
sessionStrategy: [CAPSessionStrategy activityBased]
];
// `fixed` session strategy
[CAPLogger
// ...
sessionStrategy: [CAPSessionStrategy fixed]
];
// `activityBased` session strategy
init("<your-api-key>", SessionStrategy.Activity);
// `fixed` session strategy
init("<your-api-key>", SessionStrategy.Fixed);
// `activityBased` session strategy
init("<your-api-key>", SessionStrategy.Activity);
// `fixed` session strategy
init("<your-api-key>", SessionStrategy.Fixed);
Retrieving Session ID¶
The current session identifier can be retrieved when needed using the Logger
getter. Session ID is only available after the Logger has been started.
Logger.sessionId
Logger.getSessionId();
Logger.sessionID
[CAPLogger sessionID];
import { getSessionId } from '@bitdrift/react-native';
getSessionId();
Retrieving Session URL¶
Similarly, the full session permalink URL can be retrieved using the Logger
getter. This is particularly helpful when integrating with external systems. Session URL is only available after the Logger has been started.
Logger.sessionUrl
Logger.getSessionUrl();
Logger.sessionURL
import { getSessionUrl } from '@bitdrift/react-native';
getSessionUrl();
Generating New Session ID¶
It may be desirable to generate a new session identifier in response to specific user action(s) (e.g., user logging out) or other events.
A new session identifier can be generated using a simple method call:
Logger.startNewSession()
Logger.startNewSession();
Logger.startNewSession()
Depending on the session strategy used, the new session identifier is generated in the following way:
ActivityBased
(Kotlin) /activityBased
(Swift) /Activity: A random, unique session identifier is generated each time the
startNewSession` method is called.Fixed
(Kotlin) /fixed
(Swift): A session identifier is set to the identifier retrieved using thesessionIdGenerator
(Kotlin) lambda /sessionIDGenerator
(Swift) closure. This closure is passed to the initializer of theFixed
(Kotlin) /fixed
(Swift) session strategy.
For the Fixed
(Kotlin) / fixed
(Swift) session strategy, a session ID generator can be passed in the following way:
var counter = 0;
Logger.start(
// ...
sessionStrategy = SessionStrategy.Fixed(sessionIdGenerator = { (++counter).toString() })
)
println(Logger.sessionId) // prints "1"
Logger.startNewSession()
println(Logger.sessionId) // prints "2"
AtomicInteger counter = new AtomicInteger(0);
Logger.start(
// ...
new SessionStrategy.Fixed(() -> Integer.toString(counter.incrementAndGet()))
);
println(Logger.getSessionId()); // prints "1"
Logger.startNewSession();
println(Logger.getSessionId()); // prints "2"
var counter = 0
Logger.start(
// ...
sessionStrategy: .fixed {
counter += 1;
return \"(counter)"
}
)
print(Logger.sessionID) // prints "1"
Logger.startNewSession()
print(Logger.sessionID) // prints "2"
Observing Session ID Changes¶
In the case of the ActivityBased
(Kotlin) / activityBased
(Swift) session strategy, the session identifier is managed on behalf of SDK customers and can change without their explicit request. For this reason, the SDK provides a way for customers to register for session identifier changes specific to this session strategy type:
Logger.start(
// ...
sessionStrategy = SessionStrategy.ActivityBased(
inactivityThresholdMins = 30,
onSessionIdChanged = { newSessionId ->
// Do something with the new session ID
}
)
)
Logger.start(
// ...
new SessionStrategy.ActivityBased(
30, //inactivityThresholdMins
newSessionId -> {
// Do something with the new session ID
return Unit.INSTANCE;
}
)
);
Logger.start(
// ...
sessionStrategy: .activityBased(
inactivityThresholdMins: 30,
onSessionIDChanged: { newSessionID in
// Do something with the new session ID
}
)
)
Automatic Instrumentation¶
Log Events¶
Upon start, the SDK automatically tracks a set of logs by observing certain app and device states.
Event | Notes |
---|---|
App Close | Triggered by ProcessLifecycleOwner Lifecycle.Event.ON_STOP |
App Exit | Emitted each time the SDK is started and Android returns a result from the getHistoricalProcessExitReasons method containing details about the ApplicationExitInfo . |
App Launch | Triggered by ProcessLifecycleOwner Lifecycle.Event.ON_CREATE |
App Launch TTI | Manually emitted by the SDK customer when the app becomes interactive or when a user interacts with the app, depending on the desired behavior. |
App Moved To Background | Triggered by ProcessLifecycleOwner Lifecycle.Event.ON_PAUSE |
App Moved To Foreground | Triggered by ProcessLifecycleOwner Lifecycle.Event.ON_RESUME |
App Open | Triggered by ProcessLifecycleOwner Lifecycle.Event.ON_START |
App Update | Emitted each time the SDK detects an versionName or longVersionCode change. The emitted log event contains information about the app installation size. |
Fatal Issue - JVM | Triggered by intercepting uncaught exceptions |
Low Power Mode | Triggered by isPowerSaveMode |
Memory Pressure | Emitted when app memory usage exceeds ActivityManager.MemoryInfo.threshold. Replaces noisy onTrimMemory-based logging. |
Orientation Change | Emmitted each time the SDK detects a device orientation change. |
Resource Utilization | Periodically reported event with a snapshot of application's resource consumption. The information captured, among others, includes memory usage and battery level. |
SDK Started | Emitted each time the SDK is started. It contains information about the SDK version and the duration of time the start took. |
Screen View | Manually emitted by the SDK customer when a user navigates to a different screen view representation. |
Slow Rendering | Emitted whenever an application frame takes too long to render, as detected by the JankStats library2. Android defines default latency thresholds3: frames taking longer than 16 ms are classified as Slow Frames, longer than 700 ms as Frozen Frames, and frames that exceed 5 seconds are labeled as ANR (Application Not Responding) Frames. These thresholds can be customized via runtime flags. |
Thermal State Changed | Emitted each time the callbacks of OnThermalStatusChangedListener are called. |
Timezone Change | Triggered by Intent.ACTION_TIMEZONE_CHANGED |
Event | Notes |
---|---|
App Close | Triggered by UIApplicationDidEnterBackgroundNotification |
App Did Finish Launching | Triggered by didFinishLaunchingNotification |
App Launch TTI | Manually emitted by the SDK customer when the app becomes interactive or when a user interacts with the app, depending on the desired behavior. |
App Not Responding (ANR) | Emitted each time the main run loop becomes unresponsive for a specified amount of time. Default is 2 seconds but can be configured by runtime flag. |
App Open | Triggered by UIApplicationWillEnterForegroundNotification or didFinishLaunchingNotification |
App Update | Emitted each time the SDK detects a version or build number change. The emitted log event contains information about the app installation size. |
App Will Terminate | Triggered by UIApplicationWillTerminateNotification |
Low Power Mode | Triggered by isLowPowerModeEnabled |
Memory Pressure | Triggered by applicationDidReceiveMemoryWarning |
Orientation Change | Emmitted each time the SDK detects a device orientation change. |
Resource Utilization | Periodically reported event with a snapshot of application's resource consumption. The information captured, among others, includes memory usage and battery level. |
Scene Did Enter Background | Triggered by UIApplicationDidEnterBackgroundNotification |
Scene Will Enter Foreground | Triggered by UIApplicationWillEnterForegroundNotification |
Screen View | Manually emitted by the SDK customer when a user navigates to a different screen view representation. |
SDK Started | Emitted each time the SDK is started. It contains information about the duration of time the start took. |
Session Replay | Emitted each time the SDK captures session replay frame. |
Thermal State Changed | Emitted each time ProcessInfo.thermalStateDidChangeNotification notification is posted. |
Timezone Change | Triggered by NSSystemTimeZoneDidChange |
React Native supports all the out of the box events provided by the platform the app is running on, see the iOS and Android sections for more details.
Event | Notes |
---|---|
SDK Started | Emitted each time the SDK is started. It contains information about the duration of time the start took. |
Resource Reporting¶
The Capture SDK can take periodic snapshots of the resource consumption of the app on the device. The information captured, among others, includes memory usage and battery level.
Session Replay¶
The Capture SDK makes it possible to periodically capture optimized representations of the user screen that do not include Personal Identifiable Information (PII) like text or images.
The feature captures the application screen on a periodic basis, is enabled by default, and can be disabled at any time through a remote configuration update.
The feature allows you to specify additional categorizers, which can be used to provide custom rendering for your app-specific views. For example, categorizers can inform the SDK that your custom implementation of a switch-like UI should be rendered as a switch.
Logger.start(
// ...
configuration = Configuration(
sessionReplayConfiguration = SessionReplayConfiguration(
categorizers = mapOf(
ReplayType.View to listOf("CoreUiPanel")
)
)
)
)
Info
Session Replay supports both standard Android views as well as Compose views.
HashMap categorizers = new HashMap();
List<String> names = new LinkedList<String>();
names.add("CoreUiPanel")
categorizers.put(ReplayType.View, names);
Logger.start(
// ...
new Configuration(
new SessionReplayConfiguration(
categorizers // categorizers
)
)
);
Info
Session Replay supports both standard Android views as well as Compose views.
Logger.start(
// ...
configuration: .init(
sessionReplayConfiguration: .init(
categorizers: [
"CoreUiPanel": AnnotatedView(.view, recurse: false),
]
)
)
)
Info
Session Replay supports both UIKit views as well as SwiftUI views.
TTI¶
Currently the SDK requires the customer to supply Time To Interactive (TTI) information via an explicit API. This data will be used to populate the App Launch TTI log event as well as data in the TTI instant insights dashboard.
Logger.logAppLaunchTTI(...)
Logger.logAppLaunchTTI(...);
Logger.logAppLaunchTTI(...)
import { logAppLaunchTTI } from '@bitdrift/react-native';
logAppLaunchTTI(ttiMs)
Screen Views¶
Currently the SDK requires the customer to supply which screens the users has visited via an explicit API. This data will be used to populate the User Journeys instant insights dashboard.
Logger.logScreenView("my_screen_name")
Logger.logScreenView("my_screen_name");
Logger.logScreenView("my_screen_name")
import { logScreenView } from '@bitdrift/react-native';
logScreenView('my_screen_name')
Fatal Issues and Crash Reporting¶
Refer to Fatal Issues & Crashes section for more details about how to enable automatic detection and upload of issues.
Integrations¶
Refer to Integrations section for more details about various integrations including, but not limited to, capturing of network traffic information.
Custom Logs¶
You can add custom logs using the log message API. The SDK provides methods for the five supported severity levels: trace
, debug
, info
, warning
, and error
.
The logging methods:
- Take an optional
fields
dictionary where you can pass arbitrary attributes using key-value pairs. Thefields
parameter is of typeMap<String, String>
on Android,[String: Encodable & Sendable]
in Swift (iOS), and[String: String]
in Objective-C (iOS). Refer to Fields for more details. - Take an optional instance of
Throwable
(Kotlin/Java) orError
(Swift) as one of its arguments. See Capturing Errors and Exceptions for more details. - On iOS, capture
file
,line
, andfunction
arguments for the location in code where they appear. See Capturing File, Line and Function for more details.
Logger.log(LogLevel.INFO, mapOf("key" to "value")) { "Info log" }
Logger.logTrace(mapOf("key" to "value")) { "Trace log" }
Logger.logDebug(mapOf("key" to "value")) { "Debug log" }
Logger.logInfo(mapOf("key" to "value")) { "Info log" }
Logger.logWarning(mapOf("key" to "value")) { "Warning log" }
Logger.logError(mapOf("key" to "value")) { "Error log" }
Logger.log(LogLevel.INFO, Collections.singletonMap("key", "value"), () -> "Info log");
Logger.logTrace(Collections.singletonMap("key", "value"), () -> "Trace log");
Logger.logDebug(Collections.singletonMap("key", "value"), () -> "Debug log");
Logger.logInfo(Collections.singletonMap("key", "value"), () -> "Info log");
Logger.logWarning(Collections.singletonMap("key", "value"), () -> "Warning log");
Logger.logError(Collections.singletonMap("key", "value"), () -> "Error log");
Logger.log(level: .info, "Info log", fields: ["key": "value"])
Logger.logTrace("Trace log", fields: ["key": "value"])
Logger.logDebug("Debug log", fields: ["key": "value"])
Logger.logInfo("Info log", fields: ["key": "value"])
Logger.logWarning("Warning log", fields: ["key": "value"])
Logger.logError("Error log", fields: ["key": "value"])
[CAPLogger logWithLevel:LogLevel.info message:@"Info log" fields: @{@"key": @"value"}];
[CAPLogger logTrace:@"Trace log" fields:@{@"key": @"value"}];
[CAPLogger logDebug:@"Debug log" fields:@{@"key": @"value"}];
[CAPLogger logInfo:@"Info log" fields:@{@"key": @"value"}];
[CAPLogger logWarning:@"Warning log" fields:@{@"key": @"value"}];
[CAPLogger logError:@"Error log" fields:@{@"key": @"value"}];
import { info } from '@bitdrift/react-native';
// Log line with LogLevel of Trace
trace('Hello world!', { key: 'value' });
debug('Hello world!', { key: 'value' });
info('Hello world!', { key: 'value' });
warning('Hello world!', { key: 'value' });
error('Hello world!', { key: 'value' });
import { info } from '@bitdrift/react-native';
// Log line with LogLevel of Trace
trace('Hello world!', { key: 'value' });
debug('Hello world!', { key: 'value' });
info('Hello world!', { key: 'value' });
warning('Hello world!', { key: 'value' });
error('Hello world!', { key: 'value' });
Capturing Errors and Exceptions¶
All logging methods support passing of an optional Throwable
(Kotlin/Java) / Error
(Swift) argument to help with the reporting of errors and exceptions.
For each logged Throwable
instance, the SDK adds following fields to emitted log:
_error
field with the Java exception name (it.javaClass.name
) as its value, e.g.,java.io.IOException
._error_details
field with exception message (it.message
) as its value.
Logger.log(LogLevel.INFO, throwable) { "Info log" }
Logger.logTrace(throwable) { "Trace log" }
Logger.logDebug(throwable) { "Debug log" }
Logger.logInfo(throwable) { "Info log" }
Logger.logWarning(throwable) { "Warning log" }
Logger.logError(throwable) { "Error log" }
For each logged Throwable
instance, the SDK adds following fields to emitted log:
_error
field with the Java exception name (it.javaClass.name
) as its value, e.g.,java.io.IOException
._error_details
field with exception message (it.message
) as its value.
Logger.log(LogLevel.INFO, throwable, () -> "Info log");
Logger.logTrace(throwable, () -> "Trace log");
Logger.logDebug(throwable, () -> "Debug log");
Logger.logInfo(throwable, () -> "Info log");
Logger.logWarning(throwable, () -> "Warning log");
Logger.logError(throwable, () -> "Error log");
For each logged Error
instance, the SDK adds following fields to emitted log:
_error
field with a localized description of the error (error.localizedDescription
)._error_details
field with the description of the error (String(describing: error)
)._error_info_X
field(s) for each key-value pair present inuserInfo
dictionary found after casting the passedError
instance toNSError
. For example, for auserInfo
dictionary like ["foo": "bar"], the SDK emits a field with_error_info_foo
key andbar
value.
Logger.log(level: .info, "Info log", error: error)
Logger.logTrace("Trace log", error: error)
Logger.logDebug("Debug log", error: error)
Logger.logInfo("Info log", error: error)
Logger.logWarning("Warning log", error: error)
Logger.logError("Error log", error: error)
Capturing File, Line, and Function¶
Logs captured using Swift interfaces on iOS include line file (#fileID
), line number (#line
), and function (#function
) information as their captured fields. That can be disabled by passing the nil
values for file
, line
and function
arguments explicitly.
// Logs emitted with file, line, and function information attached as fields.
Logger.log(level: .info, "Info log", fields: ["key": "value"])
Logger.logInfo(mapOf("key" to "value")) { "Info log" }
// Disable capturing of file, line, and function information.
Logger.log(level: .info, "Info log", file: nil, line: nil, function: nil fields: ["key": "value"])
Logger.logInfo(mapOf("key" to "value"), file: nil, line: nil, function: nil) { "Info log" }
Fields¶
For extra context, every log is accompanied with an optional fields
dictionary.
It's encouraged to put changing parts of your log messages inside of fields
argument as it makes it simpler to match on such attributes
while creating workflows. For example, instead of logging message "Timezone changed from X to Y" log "Timezone changed" message with
fields two fields: from
and to
.
Logger.log(LogLevel.INFO, mapOf("from" to "UTC", "to" to "PST")) { "Timezone changed" }
HashMap fields = new HashMap();
fields.put("from", "UTC");
fields.put("to", "PST");
Logger.log(LogLevel.INFO, fields, () -> "Timezone changed");
Logger.log(level: .info, "Timezone changed", fields: ["from": "UTC", "to": "PST"])
[CAPLogger logWithLevel:LogLevel.info message:@"Timezone changed" fields: @{@"from": @"UTC", @"to": @"PST"}];
import { info } from '@bitdrift/react-native';
info('Timezone changed', { from: 'UTC', to: "PST" });
import { info } from '@bitdrift/react-native';
info('Timezone changed', { from: 'UTC', to: "PST" });
There are three different ways to attach fields to logs. Below is the list, in order of priority, indicating which methods have precedence over others (fields provided by methods higher in the list override those provided by methods lower in the list):
- Using the
fields
argument in a specificlog(...)
method: This gives the greatest level of control on a per-log basis but has the highest performance impact. Refer to Custom Fields for more details. - Using the
addField(...)
method: This attaches fields to all logs emitted by the SDK after the method call. It is the most performant way to add fields to logs. Refer toField Addition and Removal Methods for more details. - Implementing
FieldsProvider
protocol/interface: This approach is more flexible but less direct. Refer to Field Providers for more details.
Default Fields¶
Every log emitted by the SDK is tagged with extra out-of-the-box attributes related to the device and app state. Below is the their list:
Field Name | Field Key | Notes |
---|---|---|
App Identifier | app_id |
|
App Version | app_version |
The value of the versionName attribute of the PackageInfo |
App Version Code | _app_version_code |
The value obtained using getLongVersionCode or versionCode accessors. |
Carrier | carrier |
|
Foreground | foreground |
|
Locale | _locale |
The string value of the getLocales method call. |
Log Level | level |
|
Network Type | network_type |
|
Network Quality | _network_quality |
Expose if the app is Offline based on its ability to reach the bitdrift ingest API. The field is not shown if the app is online. Note that any successful network request will identify the app as online |
OS | os |
|
OS Version | os_version |
The value of RELEASE property. |
Radio Type | radio_type |
Field Name | Field Key | |
---|---|---|
App Identifier | app_id |
|
App Version | app_version |
The value stored under the CFBundleShortVersionString key of the info dictionary of the main Bundle. |
Build Number | _build_number |
The valued stored under the kCFBundleVersionKey key of the infoDictionary property of the main Bundle. |
Foreground | foreground |
|
Locale | _locale |
The value of the Locale.current.identifier property. |
Log Level | level |
|
Network Type | network_type |
|
Network Quality | _network_quality |
Expose if the app is Offline based on its ability to reach the bitdrift ingest API. The field is not shown if the app is online. Note that any successful network request will identify the app as online |
OS | os |
|
OS Version | os_version |
The value of systemVersion property. |
Radio Type | radio_type |
React Native supports all the out of the box fields provided by the platform the app is running on, see the iOS and Android sections for more details.
Custom Fields¶
The SDK allows all but the following field names to be used by the customers of the SDK:
- Fields with keys that conflict with keys of the default fields emitted by the SDK. This is enforced for all fields and offending fields are dropped (a warning message is printed in the console when that happens).
- Fields whose names start with "_" (underscore character). This is enforced for global fields
and offending fields are dropped (a warning message is printed in the console when that happens). The rule is going to be
enforced for
fields
arguments of custom logs in the future.
Providing Fields with Logging Methods¶
You can attach arbitrary fields to logs emitted by and with the use of the SDK.
Logger.log(LogLevel.INFO, mapOf("key" to "value")) { "Info log" }
Logger.logTrace(mapOf("key" to "value")) { "Trace log" }
Logger.logDebug(mapOf("key" to "value")) { "Debug log" }
Logger.logInfo(mapOf("key" to "value")) { "Info log" }
Logger.logWarning(mapOf("key" to "value")) { "Warning log" }
Logger.logError(mapOf("key" to "value")) { "Error log" }
Logger.log(LogLevel.INFO, Collections.singletonMap("key", "value"), () -> "Info log");
Logger.logTrace(Collections.singletonMap("key", "value"), () -> "Trace log");
Logger.logDebug(Collections.singletonMap("key", "value"), () -> "Debug log");
Logger.logInfo(Collections.singletonMap("key", "value"), () -> "Info log");
Logger.logWarning(Collections.singletonMap("key", "value"), () -> "Warning log");
Logger.logError(Collections.singletonMap("key", "value"), () -> "Error log");
Logger.log(level: .info, "Info log", fields: ["key": "value"])
Logger.logTrace("Trace log", fields: ["key": "value"])
Logger.logDebug("Debug log", fields: ["key": "value"])
Logger.logInfo("Info log", fields: ["key": "value"])
Logger.logWarning("Warning log", fields: ["key": "value"])
Logger.logError("Error log", fields: ["key": "value"])
[CAPLogger logWithLevel:LogLevel.info message:@"Info log" fields: @{@"key": @"value"}];
[CAPLogger logTrace:@"Trace log" fields:@{@"key": @"value"}];
[CAPLogger logDebug:@"Debug log" fields:@{@"key": @"value"}];
[CAPLogger logInfo:@"Info log" fields:@{@"key": @"value"}];
[CAPLogger logWarning:@"Warning log" fields:@{@"key": @"value"}];
[CAPLogger logError:@"Error log" fields:@{@"key": @"value"}];
import { info } from '@bitdrift/react-native';
// Log line with LogLevel of Trace
trace('Hello world!', { key: 'value' });
debug('Hello world!', { key: 'value' });
info('Hello world!', { key: 'value' });
warning('Hello world!', { key: 'value' });
error('Hello world!', { key: 'value' });
import { info } from '@bitdrift/react-native';
// Log line with LogLevel of Trace
trace('Hello world!', { key: 'value' });
debug('Hello world!', { key: 'value' });
info('Hello world!', { key: 'value' });
warning('Hello world!', { key: 'value' });
error('Hello world!', { key: 'value' });
Global Fields¶
The SDK supports two ways of adding log fields to attach to every emitted log. Each of these methods manages a separate pool of logs, ensuring that the method of managing fields does not impact those managed by the other method.
Note
Global fields are not persisted to disk and thus do not survive process restarts, so it is possible for these values to change within a session depending on your session management strategy.
Field Addition and Removal Methods¶
The logger exposes addField(...)
and removeField(...)
methods, which can be used to control the list of
global fields added to emitted logs.
Logger.addField("key", "value")
Logger.removeField("key")
Logger.addField("key", "value");
Logger.removeField("key");
Logger.addField(withKey: "key", value: "value")
Logger.removeField(withKey: "key")
import { addField, removeField } from '@bitdrift/react-native';
addField('key', 'value');
removeField('key');
Field Providers¶
The start
method takes an optional list of fieldProviders
where you can implement your own logic for retrieving the data you want to pass.
In cases of key conflicts (where two or more fields share the same key), the earlier a field provider appears in the list of registered providers, the higher priority the field it returns is given.
// Attach your own custom user_id field to each log
class CustomUserIdProvider : FieldProvider {
override fun invoke(): Fields {
return mapOf("user_id" to "<your_custom_user_id>")
}
}
Logger.start(
// ...
fieldProviders = listOf(CustomUserIdProvider()),
)
// Attach your own custom user_id field to each log
class CustomUserIdProvider implements FieldProvider {
@Override
public Map<String, ? extends String> invoke() {
return Collections.singletonMap("user_id", "<your_custom_user_id>");
}
}
Logger.start(
// ...
Collections.singletonList(new CustomUserIdProvider()) //fieldProviders
);
// Attach your own custom user_id field to each log
final class CustomUserIdProvider: FieldProvider {
func getFields() -> Fields {
["user_id": "<your_custom_user_id>"]
}
}
Logger.start(
// ...
fieldProviders: [CustomUserIdProvider()],
)
Tip
user_id
is a one-of-a-kind special field. Providing it as shown above will cause it to appear in the Timeline header.
HTTP Traffic Logs¶
The SDK offers specialized logging APIs for capturing information about network requests. Exposed in the form of log
method calls the APIs can be used to manually log information about each request and response.
Tip
The recommended way to add Capture logs for network traffic in an app is through Capture Networking Integrations integrations.
val requestInfo = HttpRequestInfo(
host = "<endpoint_host>",
path = HttpUrlPath("<endpoint_path>", "<endpoint_path_template>"),
method = "<http_request_method>",
headers = emptyMap(),
)
Logger.log(requestInfo)
val responseInfo = HttpResponseInfo(
request = requestInfo,
response = HttpResponse(
result = HttpResponse.HttpResult.SUCCESS,
statusCode = 200,
headers = emptyMap(),
),
durationMs = 500,
metrics = HTTPRequestMetrics(
requestBodyBytesSentCount = 1,
responseBodyBytesReceivedCount = 2,
requestHeadersBytesCount = 3,
responseHeadersBytesCount = 3,
dnsResolutionDurationMs = 1234,
),
)
Logger.log(responseInfo)
HttpRequestInfo requestInfo = new HttpRequestInfo(
"<endpoint_host>", //host
HttpUrlPath("<endpoint_path>", "<endpoint_path_template>"), //path
"<http_request_method>" //method
Collections.emptyMap(), // HTTP headers
);
Logger.log(requestInfo);
HttpResponseInfo responseInfo = new HttpResponseInfo(
requestInfo, //request
new HttpResponse(
HttpResponse.HttpResult.SUCCESS, //result
200 //statusCode,
Collections.emptyMap(), // HTTP headers
),
500, //durationMs
new HttpRequestMetrics(
1, // requestBodyBytesSentCount
2, // responseBodyBytesReceivedCount
3, // requestHeadersBytesCount
4, // responseHeadersBytesCount
1234 // dnsResolutionDurationMs
)
);
Logger.log(responseInfo);
let requestInfo = HTTPRequestInfo(
host: "<endpoint_host>",
path: HTTPURLPath(value: "<endpoint_path>", template: "endpoint_path_template"),
method: "<http_request_method>",
headers: [:],
)
Logger.log(requestInfo)
let responseInfo = HTTPResponseInfo(
requestInfo: requestInfo,
response: .init(
result: .success,
statusCode: 200,
headers: [:],
error: nil,
),
duration: 0.5,
metrics: .init(
requestBodyBytesSentCount: 1,
responseBodyBytesReceivedCount: 2,
requestHeadersBytesCount: 3,
responseHeadersBytesCount: 4,
dnsResolutionDuration: 1.23
)
)
Logger.log(responseInfo)
Fields¶
HTTP logs emitted with the use of HttpRequestInfo
(Android) / HTTPRequestInfo
(iOS) and HttpResponseInfo
(Android) / HTTPResponseInfo
(iOS) types contain multiple HTTP-specific fields outlined below.
The description of the specific fields discusses the intended values of these fields and how the Capture OkHttp (Android) and URLSession (iOS) integrations use these fields.
Request and response info objects can be manually initialized and logged by a customer of the Capture SDK. This flexibility means that depending on the values of the parameters passed to the initializers of these objects, the final values of the outlined fields may differ from what is described below.
When logging network traffic manually, reuse the same instance of HttpRequestInfo
(Android) or HTTPRequestInfo
(iOS) for logging both request and response information.
HTTP Request Fields¶
Field Name | Field Key | Example Values | Notes |
---|---|---|---|
Method | _method |
GET , POST |
|
Host | _host |
bitdrift.io |
|
Path | _path |
/v1/ping/123 |
|
Path Template | _path_template |
/v1/ping/<id> |
A version of path with high-cardinality portions (if any) replaced with a a string placeholder (e.g., <id> ). If a path template is not provided at the time of initialization, the SDK tries to find and replace high-cardinality portions of the path with the <id> placeholder on its own. |
Protocol | _protocol |
http/1.0 , http/1.1 , h2 |
The HTTP Protocol of the Request. |
Query | _query |
q=foo&source=bar |
|
Request Body Size (Expected) | _request_body_bytes_expected_to_send_count |
123 | The number of body bytes expected to be sent for a given request. On Android, it does not take into account any body payload modifications performed by the Interceptor chain (e.g. compression). |
Span ID | _span_id |
8bcbbef6-7b3a-44c7-8c8e-47c5c15f2412 | Each request-response pair shares the same span ID value. |
HTTP Response Fields¶
Field Name | Field Key | Example Values | Notes |
---|---|---|---|
Init Duration | _fetch_init_duration_ms |
55 | Client time overhead before connecting. |
TCP Duration | _tcp_duration_ms |
97 | The cumulative duration of all TCP handshakes performed during the execution of a given HTTP request. |
TLS Duration | _tls_duration_ms |
102 | The cumulative duration of all TLS handshakes performed during the execution of a given HTTP request. |
DNS Duration | _dns_duration_ms |
223 | The duration of time the DNS query(ies) for a given request took. Present only on response logs for requests that triggered DNS lookups, as opposed to those that used previously cached DNS results (a common case). |
Response Latency | _response_latency_ms |
865 | The cumulative duration of all responses from the time the request is sent to the time we get the first byte from the server. |
Duration | _duration_ms |
1342 | The total roundtrip duration of a request / response pair. |
Error Code | _error_code |
-1009 | Presents a code for client-side errors in cases where a request fails due to issues such as timeout or cancellation. |
Error Message | _error_message |
The Internet connection appears to be offline. | |
Host | _host |
bitdrift.io |
|
Method | _method |
GET , POST |
|
Path | _path |
/v1/ping/123 |
|
Path Template | _path_template |
/v1/ping/<id> |
A version of path with high-cardinality portions (if any) replaced with a a string placeholder (e.g., <id> ). If a path template is not provided at the time of initialization, the SDK tries to find and replace high-cardinality portions of the path with the <id> placeholder on its own. |
Protocol | _protocol |
http/1.0 , http/1.1 , h2 |
The HTTP Protocol of the Response. |
Query | _query |
q=foo&source=bar |
|
Request Body Size (Sent) | _request_body_bytes_sent_count |
1234 | The number of request body bytes sent over-the-wire. It takes into account compression if one is used by the app. |
Request Headers Size | _request_headers_bytes_count |
1234 | The number of request headers bytes sent. This represents the over-the-wire bytes on iOS and an estimate of the over-the-wire bytes before they are compressed with HPACK on Android. |
Response Body Size | _response_body_bytes_received_count |
1234 | The number of response body bytes received over-the-wire. |
Response Headers Size | _response_headers_bytes_count |
1234 | The number of response headers bytes sent. This represents the over-the-wire bytes on iOS and an estimate of the over-the-wire bytes before they are compressed with HPACK on Android. |
Result | _result |
success , failure , canceled |
|
Span ID | _span_id |
8bcbbef6-7b3a-44c7-8c8e-47c5c15f2412 | Each request-response pair shares the same span ID value. |
Status Code | _status_code |
200, 401 | Present only if client receives a response from a server. |
Spans¶
A span represents a unit of work with defined start and end logs. Spans are useful when instrumenting blocks of time, e.g., "How long was a loading spinner displayed?"
val span = Logger.startSpan(
"loading_spinner",
LogLevel.INFO
mapOf(),
)
// ...
span?.end(SpanResult.SUCCESS, mapOf())
// or:
Logger.trackSpan("operation", LogLevel.INFO) {
operation()
}
Optional<Span> span = Logger.startSpan(
"loading_spinner",
LogLevel.INFO,
new HashMap(),
)
// ...
span.ifPresent(s -> s.end(SpanResult.SUCCESS, new HashMap()))
let span = Logger.startSpan(
name: "loading_spinner",
level: .info,
fields: [:]
)
// ...
span?.end(.success, fields: [:])
The SDK calculates and attaches information about the span duration each time a span ends.
Info
See the Timeline Waterfalls section for seeing how this information is displayed in the product.
Spans Hierarchy¶
Spans can be nested hierarchically by providing a parentID to the start call. The parent span ID is the ID of the span that started the child span.
graph TD
A[Span A] --> B[Span B]
A --> C[Span C]
B --> D[Span D]
C --> E[Span E]
In the example above, Span A is the parent of Spans B and C. Span B is the parent of Span D, and Span C is the parent of Span E.
This is specially useful when spans are rendered in the waterfall chart of the Timeline view.
Custom start & end time (advanced)¶
The SDK allows you to provide custom start and end times for spans. This is useful when you have a specific time source or timing heuristic that you want to use for your spans. These custom times are also used to display the logs in the correct position in the Timeline view. When providing custom times, you need to provide both the start and end times, failing to provide one of them will result in the span being tracked using system time.
val span = Logger.startSpan(
"loading_spinner",
LogLevel.INFO,
mapOf(),
startTimeMs = 1234567890000L,
)
// ...
span?.end(
SpanResult.SUCCESS,
mapOf(),
endTimeMs = 1234567900000L,
)
let span = Logger.startSpan(
name: "loading_spinner",
level: .info,
fields: [:],
startTimeInterval: 1234567890.0
)
// ...
span?.end(.success, fields: [:], endTimeInterval: 1234567900.0)
Warning
Please note that this is an advanced feature and should be used with caution, as workflows match in the order logs are received, regardless of the provided time.
Fields¶
Span logs contain multiple out-of-the-box fields outlined below.
Span Start Fields¶
Field Name | Field Key | Example Values | Notes |
---|---|---|---|
Span Parent | _span_parent_id |
foobar-7b3a-44c7-8c8e-47c5c15f2412 | Spans can be nested hirarchically. The parent span ID is the ID of the span that started the child span. |
Span ID | _span_id |
8bcbbef6-7b3a-44c7-8c8e-47c5c15f2412 | Each span start-end logs pair shares the same span ID value. |
Span Name | _span_name |
spinner_loading |
|
Span Type | _span_type |
start |
Span Start End¶
Field Name | Field Key | Example Values | Notes |
---|---|---|---|
Duration | _duration_ms |
123 | |
Result | _result |
success , failure , canceled , unknown |
The span result |
Span Parent | _span_parent_id |
foobar-7b3a-44c7-8c8e-47c5c15f2412 | Spans can be nested hirarchically. The parent span ID is the ID of the span that started the child span. |
Span ID | _span_id |
8bcbbef6-7b3a-44c7-8c8e-47c5c15f2412 | Each span start-end logs pair shares the same span ID value. |
Span Name | _span_name |
spinner_loading |
|
Span Type | _span_type |
end |
Date Provider¶
Capture SDK annotates each of the emitted logs with a timestamp.
By default, the Capture Logger
queries the current time using system provided APIs - Date()
in Kotlin/Swift.
A custom DateProvider
instance can be provided at the Logger
start time for cases when another time source is desirable (e.g., you have NTP time source in your app that you want to use for logs emitted by Capture too).
class CustomDateProvider : DateProvider {
override fun invoke(): Date {
return Date() // Replace with your own time source.
}
}
Logger.start(
// ...
dateProvider = CustomDateProvider(),
)
class CustomDateProvider implements DateProvider {
@Override
public Date invoke() {
return new Date(); //Replace with your own time source.
}
}
Logger.start(
// ...
new CustomDateProvider() //dateProvider
);
final class CustomDateProvider: DateProvider {
func getDate() -> Date {
return Date()
}
}
Logger.start(
// ...
dateProvider: CustomDateProvider(),
)
SDK Version¶
Capture SDK exposes a way to query the current version of the SDK at runtime. This is provided as metadata that can be helpful to troubleshoot issues should they arise.
Capture.Logger.sdkVersion
Capture.Logger.getSdkVersion()
Capture.Logger.sdkVersion
[CAPLogger sdkVersion]
Device¶
The Capture SDK can be connected to bd tail
session through the use of a Device Identifier or Device Code.
Identifier¶
The device identifier can be obtained using the device identifier property on the Capture Logger. The identifier remains consistent as long as the host application is not reinstalled.
It can be used with the deviceid operator in the bd tail CLI command to stream logs directly from the device in real-time.
Logger.deviceId
Logger.getDeviceId()
Logger.deviceID
[CAPLogger deviceID]
import { getDeviceId } from '@bitdrift/react-native';
getDeviceId().then((deviceId) => {
// Use deviceId
}).catch((error) => {
// Handle error
});
Code¶
Calling Logger.createTemporaryDeviceCode
will asynchronously return a device code that's valid for a limited duration of time (around a day).
It can be used with the devicecode
operator in the bd tail CLI command to stream logs directly from the device in real-time.
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.onSuccess
import io.bitdrift.capture.Capture.Logger
// Result uses https://github.com/michaelbull/kotlin-result
Logger.createTemporaryDeviceCode(completion = { result ->
result.onSuccess { deviceCode ->
// Display code
}
result.onFailure { error ->
// Handle error
}
})
import com.github.michaelbull.result.OnKt;
import io.bitdrift.capture.Capture.Logger;
// Result uses https://github.com/michaelbull/kotlin-result
Logger.createTemporaryDeviceCode(result -> {
OnKt.onSuccess(result, deviceCode -> {
// Display code
return null;
});
OnKt.onFailure(result, error -> {
// Handle error
return null;
});
return null;
});
Logger.createTemporaryDeviceCode { result in
switch result {
case .success(let deviceCode):
// Display code
case .failure(let error):
// Handle error
}
}
import { generateDeviceCode } from '@bitdrift/react-native';
generateDeviceCode().then((deviceCode) => {
// Display code
}).catch((error) => {
// Handle error
});