---
Source: https://measure.sh/docs
---
# Introduction
Measure helps mobile teams monitor and fix crashes, ANRs, bugs, and performance issues.
Measure collects crashes, ANRs, bug reports, session replays, performance
traces and network activity from your mobile apps and lays them out in a unified dashboard making it easy to debug production bugs and performance issues.
Choose your app platform to get started:
---
Source: https://measure.sh/docs/getting-started/android
---
# Android
Integrate Measure SDK in your Android app.
### Minimum requirements \[#minimum-requirements]
| Name | Version |
| --------------------- | --------------- |
| Android Gradle Plugin | `8.1.0` |
| JDK | `17` |
| Compile SDK | `34` |
| Min SDK | `21` (Lollipop) |
| SDK Version | Minimum Required Self-host Version |
| ------------------- | ---------------------------------- |
| >= `0.16.0` | `0.10.0` |
| `0.13.0` -`0.15.1` | `0.9.0` |
| `0.10.0` - `0.12.0` | `0.6.0` |
| `0.9.0` | `0.5.0` |
## 1. Get the credentials \[#1-get-the-credentials]
Create a new app in the *Apps* section on the dashboard, copy its `API URL` and `API Key`, and add them to your app's `AndroidManifest.xml`.
```xml
```
## 2. Add the Gradle dependency \[#2-add-the-gradle-dependency]
```kotlin
// In your app/build.gradle.kts
dependencies {
implementation("sh.measure:measure-android:0.20.0")
}
```
## 3. Add the Gradle plugin \[#3-add-the-gradle-plugin]
The plugin instruments your app at build time and uploads ProGuard/R8 mapping files to de-obfuscate crash and ANR stack traces.
```kotlin
// In your app/build.gradle.kts
plugins {
id("sh.measure.android.gradle") version "0.14.0"
}
```
## 4. Initialize the SDK \[#4-initialize-the-sdk]
Initialize as early as possible to capture early crashes and accurate launch time metrics.
```kotlin
// In your Application.onCreate()
import sh.measure.android.Measure
import sh.measure.android.config.MeasureConfig
Measure.init(
this, MeasureConfig()
)
```
## 5. Verify installation \[#5-verify-installation]
Add a test crash after `Measure.init`, run the app, and confirm it reaches your dashboard.
```kotlin
import android.os.Handler
import android.os.Looper
// In Application.onCreate(), after Measure.init().
// The 2-second delay gives the SDK time to flush the crash event.
// Remove this after the crash appears in your dashboard.
Handler(Looper.getMainLooper()).postDelayed({
throw RuntimeException("Test crash from Measure")
}, 2000)
```
Remove the test crash code once you've confirmed the crash appears in your dashboard.
---
Source: https://measure.sh/docs/getting-started/ios
---
# iOS
Integrate the Measure SDK in your iOS app.
### Minimum requirements \[#minimum-requirements]
| Name | Version |
| ----------------------- | ------- |
| Xcode | 15.0+ |
| Minimum iOS Deployments | 13.0+ |
| Swift Version | 5.10+ |
| SDK Version | Minimum Required Self-host Version |
| ----------- | ---------------------------------- |
| >=0.1.0 | 0.6.0 |
| >=0.7.0 | 0.9.0 |
## 1. Get the credentials \[#1-get-the-credentials]
Create a new app in the *Apps* section on the dashboard and copy its `API URL` and `API Key`.
## 2. Install the SDK \[#2-install-the-sdk]
```swift
// In Package.swift
.package(url: "https://github.com/measure-sh/measure.git", branch: "ios-v0.13.1")
```
```ruby
# In your Podfile
pod 'measure-sh'
```
MeasureSDK supports static linking only. If your Podfile uses `use_frameworks!`, install the [`cocoapods-pod-linkage`](https://github.com/microsoft/cocoapods-pod-linkage) plugin (`gem install cocoapods-pod-linkage`) and link `measure-sh` statically:
```ruby
# In your Podfile
plugin 'cocoapods-pod-linkage'
target 'YourApp' do
use_frameworks!
pod 'measure-sh', :linkage => :static
end
```
## 3. Initialize the SDK \[#3-initialize-the-sdk]
Initialize as early as possible to capture early crashes and accurate launch time metrics.
```swift
// In your AppDelegate's application(_:didFinishLaunchingWithOptions:)
import Measure
let clientInfo = ClientInfo(apiKey: "", apiUrl: "")
let config = BaseMeasureConfig()
Measure.initialize(with: clientInfo, config: config)
```
```objc
// In your AppDelegate's application:didFinishLaunchingWithOptions:
#import
ClientInfo *clientInfo = [[ClientInfo alloc] initWithApiKey:@"" apiUrl:@""];
BaseMeasureConfig *config = [[BaseMeasureConfig alloc]
initWithEnableLogging:YES
autoStart:YES
requestHeadersProvider:NULL
maxDiskUsageInMb:50
enableFullCollectionMode:NO
enableDiagnosticMode:NO
enableDiagnosticModeGesture:NO];
[Measure initializeWith:clientInfo config:config];
```
## 4. Verify installation \[#4-verify-installation]
Add a test crash after `Measure.initialize`, run the app, and confirm it reaches your dashboard.
```swift
// In your AppDelegate, after Measure.initialize.
// The 2-second delay gives the SDK time to flush the crash event.
// Remove this after the crash appears in your dashboard.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
fatalError("Test crash from Measure")
}
```
Remove the test crash code once you've confirmed the crash appears in your dashboard.
---
Source: https://measure.sh/docs/getting-started/flutter
---
# Flutter
Integrate the Measure SDK in your Flutter app on Android and iOS.
The Flutter SDK supports Android and iOS targets. It depends on the native Android and iOS SDKs, so their
minimum requirements apply here too.
### Minimum requirements \[#minimum-requirements]
| Name | Version |
| --------------- | -------- |
| Flutter | `3.27` |
| Measure Android | `0.20.0` |
| Measure iOS | `0.13.1` |
## 1. Get the credentials \[#1-get-the-credentials]
Create a new app in the *Apps* section on the dashboard and copy its `API URL` and `API Key`.
**Cross-platform apps need a unique API key for each platform they target.** To integrate another platform,
create a new app on Measure with a different API key.
## 2. Add the SDK \[#2-add-the-sdk]
```yaml
# In pubspec.yaml
dependencies:
measure_flutter: ^0.7.0
```
## 3. Initialize the SDK \[#3-initialize-the-sdk]
```dart
// In main()
import 'package:measure_flutter/measure_flutter.dart';
Future main() async {
await Measure.instance.init(
() => runApp(MeasureWidget(child: MyApp())),
config: const MeasureConfig(),
);
}
```
## 4. Set up the native SDKs \[#4-set-up-the-native-sdks]
Flutter depends on the native Android and iOS SDKs. Configure each platform in its native project.
### Android \[#android]
Add your credentials to `android/app/src/main/AndroidManifest.xml`:
```xml
```
Initialize the SDK in your Android `Application` class. Create one and register it with `android:name` in the manifest if your app doesn't have one yet.
```kotlin
// In Application.onCreate()
import sh.measure.android.Measure
import sh.measure.android.config.MeasureConfig
Measure.init(this, MeasureConfig())
```
### iOS \[#ios]
Measure's iOS SDK must be linked statically and will not work correctly with dynamic linking. How this is handled depends on the iOS dependency manager.
With Swift Package Manager (default from Flutter 3.44), `measure-sh` is linked statically by default and no extra configuration is required.
With CocoaPods, set your `Runner` target in `ios/Podfile` to static linkage and add the pod, then run `pod install` from the `ios` directory:
```ruby
target 'Runner' do
use_frameworks! :linkage => :static
pod 'measure-sh'
end
```
Initialize the SDK in `application(_:didFinishLaunchingWithOptions:)` in `ios/Runner/AppDelegate.swift`:
```swift
import Measure
let clientInfo = ClientInfo(apiKey: "YOUR_API_KEY", apiUrl: "YOUR_API_URL")
Measure.initialize(with: clientInfo, config: BaseMeasureConfig())
```
## 5. Verify installation \[#5-verify-installation]
Add a test crash after `Measure.instance.init`, run the app, and confirm it reaches your dashboard.
```dart
// In main(), after Measure.instance.init.
// The 2-second delay gives the SDK time to flush the crash event.
// Remove this after the crash appears in your dashboard.
Future.delayed(const Duration(seconds: 2), () {
throw Exception('Test crash from Measure');
});
```
Remove the test crash code once you've confirmed the crash appears in your dashboard.
---
Source: https://measure.sh/docs/getting-started/react-native
---
# React Native
Integrate the Measure SDK in your React Native app on Android and iOS.
The React Native SDK supports both **Expo** and **React Native** projects on Android and iOS. It depends on the
native Android and iOS SDKs, so their minimum requirements apply here too.
### Minimum requirements \[#minimum-requirements]
| Name | Version |
| --------------- | -------- |
| React Native | `0.73.0` |
| React | `18.2.0` |
| Measure Android | `0.20.0` |
| Measure iOS | `0.13.1` |
## 1. Get the credentials \[#1-get-the-credentials]
Create a new app in the *Apps* section on the dashboard and copy its `API URL` and `API Key`.
**Cross-platform apps need a unique API key for each platform they target.** To integrate another platform,
create a new app on Measure with a different API key.
## 2. Add the SDK \[#2-add-the-sdk]
```sh
# In your project root
npm install @measuresh/react-native@0.3.0
```
## 3. Add the config plugin \[#3-add-the-config-plugin]
For Expo projects, add the Measure config plugin to `app.json` (or `app.config.js`):
```json
{
"expo": {
"plugins": [
[
"@measuresh/react-native",
{
"androidApiKey": "",
"androidApiUrl": "",
"iosApiKey": "",
"iosApiUrl": ""
}
]
]
}
}
```
Apply the changes:
```sh
npx expo prebuild
```
Not using Expo? [Set up a vanilla React Native project](#vanilla-react-native) instead.
## 4. Initialize the SDK \[#4-initialize-the-sdk]
```typescript
// In your app entry (e.g. App.tsx)
import { Measure, MeasureConfig } from "@measuresh/react-native";
const config = new MeasureConfig({});
await Measure.init({ config });
```
## 5. Verify installation \[#5-verify-installation]
Add a test crash after `Measure.init`, run the app, and confirm it reaches your dashboard.
```typescript
// After Measure.init.
// The 2-second delay gives the SDK time to flush the crash event.
// Remove this after the crash appears in your dashboard.
setTimeout(() => {
throw new Error("Test crash from Measure");
}, 2000);
```
Remove the test crash code once you've confirmed the crash appears in your dashboard.
## Vanilla React Native \[#vanilla-react-native]
The React Native package already provides the native Android dependency, so there's no dependency to add by hand.
**Android**
Add your credentials to `android/app/src/main/AndroidManifest.xml`:
```xml
```
**iOS**
From the `ios` directory, run `pod install`.
By default, the `measure-sh` iOS framework is integrated via Swift Package Manager (SPM) on React Native 0.75 and above. On older React Native versions it is integrated as a CocoaPods dependency automatically. If you'd prefer to integrate it as a CocoaPods dependency instead, add the following to the top of your `Podfile`:
```ruby
$MeasureDisableSPM = true
```
`measure-sh` links statically only. If your Podfile uses `use_frameworks!`, install the [`cocoapods-pod-linkage`](https://github.com/microsoft/cocoapods-pod-linkage) plugin (`gem install cocoapods-pod-linkage`) and link `measure-sh` statically:
```ruby
plugin 'cocoapods-pod-linkage'
target '' do
use_frameworks!
pod 'measure-sh', :linkage => :static
end
```
With Android and iOS configured, continue with [Initialize the SDK](#4-initialize-the-sdk).
---
Source: https://measure.sh/docs/getting-started/kotlin-multiplatform
---
# Kotlin Multiplatform
Use Measure from shared Kotlin code on Android and iOS.
The KMP SDK provides access to Measure from shared Kotlin code (`commonMain`) on Android and iOS. It's a thin
wrapper over the native Android and iOS SDKs, so their minimum requirements apply here too.
### Minimum requirements \[#minimum-requirements]
| Name | Version |
| --------------- | -------- |
| Kotlin | `2.x` |
| Measure Android | `0.18.0` |
| Measure iOS | `0.11.0` |
## 1. Get the credentials \[#1-get-the-credentials]
Create a new app in the *Apps* section on the dashboard and copy its `API URL` and `API Key`.
**Cross-platform apps need a unique API key for each platform they target.** To integrate another platform,
create a new app on Measure with a different API key.
## 2. Add the KMP SDK \[#2-add-the-kmp-sdk]
```kotlin
// In your shared module's build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("sh.measure:measure-kmp:0.1.0")
}
}
}
```
## 3. Set up the native SDKs \[#3-set-up-the-native-sdks]
The KMP SDK does not initialize the native SDKs for you. Initialize each one in its platform entry point.
### Android \[#android]
The native Android SDK is included transitively with the `measure-kmp` dependency.
Add your credentials to your Android app module's `AndroidManifest.xml`:
```xml
```
Initialize the SDK in your `Application.onCreate()`. Create an `Application` class and register it with `android:name` in the manifest if your app doesn't have one yet.
```kotlin
// In Application.onCreate()
import sh.measure.android.Measure
import sh.measure.android.config.MeasureConfig
Measure.init(this, MeasureConfig())
```
### iOS \[#ios]
Add the native Measure iOS SDK to your iOS app using Swift Package Manager. See the [iOS guide](https://measure.sh/docs/getting-started/ios) for CocoaPods and static-linking details.
```swift
// In Package.swift, or via Xcode's package manager
.package(url: "https://github.com/measure-sh/measure.git", branch: "ios-v0.11.0")
```
Initialize the SDK in your `AppDelegate`'s `application(_:didFinishLaunchingWithOptions:)`:
```swift
import Measure
let clientInfo = ClientInfo(apiKey: "YOUR_API_KEY", apiUrl: "YOUR_API_URL")
Measure.initialize(with: clientInfo, config: BaseMeasureConfig())
```
## 4. Use the SDK from shared code \[#4-use-the-sdk-from-shared-code]
```kotlin
// In commonMain, once both native SDKs are initialized
import sh.measure.kmp.Measure
import sh.measure.kmp.attributes.StringAttr
Measure.trackScreenView("CheckoutScreen")
Measure.trackEvent(
name = "checkout_completed",
attributes = mapOf("source" to StringAttr("kmp")),
)
```
## 5. Verify installation \[#5-verify-installation]
Throw a test crash from shared code, run the app, and confirm it reaches your dashboard.
```kotlin
// Remove this after the crash appears in your dashboard.
throw RuntimeException("Test crash from Measure")
```
Remove the test crash code once you've confirmed the crash appears in your dashboard.
---
Source: https://measure.sh/docs/session-replay
---
# Session Replay
Replay each session as a chronological sequence of events with CPU and memory usage, screenshots, and layout snapshots.
A session replay shows everything that happened during a session in order: screen views, gestures, logs, network calls, and errors, alongside charts of [CPU and memory usage](https://measure.sh/docs/cpu-memory-monitoring). Screenshots and [layout snapshots](https://measure.sh/docs/gesture-tracking/layout-snapshots) capture what was on screen at the time.
## What counts as a session \[#what-counts-as-a-session]
A session is a continuous period of activity in the app. A new one starts when the SDK initializes, or when the app returns to the foreground after more than 30 seconds in the background.
To reference a session from your own logs or backend, read the current session ID with [`getSessionId`](https://measure.sh/docs/api-reference#get-session-id).
## Replay duration \[#replay-duration]
Each crash, ANR, and bug report includes the replay of the 5 minutes before it. The duration is adjustable in [Adaptive Capture](https://measure.sh/docs/adaptive-capture#session-replay-duration).
## Search sessions \[#search-sessions]
Filter the sessions list by app version, OS version, device, country, network, locale, or any user-defined attributes set in the app. Narrow further to sessions with errors, ANRs, or bug reports.
The search box matches user and session IDs, log bodies, event types, view IDs, class names, and exception traces.
---
Source: https://measure.sh/docs/error-monitoring
---
# Error Monitoring
Monitor crashes, ANRs, unhandled errors, and handled errors across your mobile app. See what broke, how often, and what the user was doing when it happened.
Measure tracks crashes, ANRs, and other errors in your app. Each error is reported with a session replay that shows the steps the user took before hitting the error. Similar errors are [grouped](https://measure.sh/docs/error-monitoring/grouping) into a single issue so you can tell how often something happens and what to fix first.
## Error severity \[#error-severity]
Every error is classified by severity.
* **Fatal** errors are ones that crash the app.
* **Unhandled** errors are ones that escape your code without crashing the app.
* **Handled** errors are ones that you catch and report manually.
## Crash reporting \[#crash-reporting]
Measure reports crashes automatically. What gets captured and how it's captured depends on your platform.
### Android \[#android]
When an uncaught exception terminates the app, it's recorded as a fatal error with the full stack trace and the session that led up to it.
If you obfuscate your code with R8, set up the [Measure Android Gradle Plugin](https://measure.sh/docs/error-monitoring/upload-symbols#android). It uploads the `mapping.txt` file so stack traces are readable.
### iOS \[#ios]
A crash report is saved the moment a crash happens and is uploaded the next time the user reopens the app. Crashes are captured using [KSCrash](https://github.com/kstenerud/KSCrash).
By default, the stack trace contains just memory addresses. Upload your dSYM files in your release pipeline or from an Xcode build phase using [these scripts](https://measure.sh/docs/error-monitoring/upload-symbols#ios).
### Flutter \[#flutter]
Most Flutter errors are Dart errors that the framework catches before they can crash the app. Measure hooks into `FlutterError.onError` and `PlatformDispatcher.onError` to record them automatically as unhandled errors. When a crash happens on the native layer, it's captured automatically as a fatal error.
If you obfuscate your Flutter code with `--obfuscate` and `--split-debug-info`, upload the generated symbol files so stack traces are readable. See [Upload symbols](https://measure.sh/docs/error-monitoring/upload-symbols#flutter) for each platform's steps.
### React Native \[#react-native]
A fatal JavaScript error is captured automatically as a crash. Non-fatal JavaScript errors and unhandled promise rejections are captured as unhandled errors. When a crash happens on the native layer, it's captured automatically as a fatal error.
Upload your JavaScript sourcemaps so stack traces are readable. See [Upload symbols](https://measure.sh/docs/error-monitoring/upload-symbols#react-native) for the steps.
### Kotlin Multiplatform \[#kotlin-multiplatform]
Crashes are captured on both the Android and iOS targets, including crashes in your shared Kotlin code.
On iOS, the stack trace includes the Kotlin frames, pointing straight to the shared code that failed:
```log
Thread 0 Crashed:
0 FrankensteinApp kfun:com.frankenstein.shared.demos$6.$invoke() (CmpScreen.kt:210)
1 FrankensteinApp kfun:com.frankenstein.shared.ActionCard$$inlined$cache$1.invoke() (CmpScreen.kt:150)
2 FrankensteinApp kfun:androidx.compose.foundation.ClickableNode#onPointerEvent() (Clickable.kt:940)
3 UIKitCore -[UIWindow sendEvent:] + 2996
...
```
## ANR reporting \[#anr-reporting]
An [ANR](https://developer.android.com/topic/performance/vitals/anr) (Application Not Responding) happens when the app's main thread is blocked for too long, for example when it can't respond to input within 5 seconds. ANRs are reported automatically.
On Android 11 and above, [ApplicationExitInfo](https://developer.android.com/reference/android/app/ApplicationExitInfo) is also collected on the next launch to record why the app exited. ANR exits include the thread dump taken by the OS.
## Track a handled error \[#track-a-handled-error]
Errors you catch and recover from can be manually reported. See [Track errors](https://measure.sh/docs/api-reference#track-errors) for the full signature.
```kotlin
import sh.measure.android.Measure
try {
methodThatThrows()
} catch (e: Exception) {
Measure.trackHandledException(e)
}
```
```swift
import Measure
// Track a Swift Error or an NSError with trackError
do {
try someThrowingFunction()
} catch {
Measure.trackError(error)
}
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
try {
methodThatThrows();
} catch (e, stackTrace) {
Measure.instance.trackHandledError(e, stackTrace);
}
```
```typescript
import { Measure } from '@measuresh/react-native';
try {
methodThatThrows();
} catch (e) {
Measure.trackError({ error: e });
}
```
```kotlin
import sh.measure.kmp.Measure
try {
methodThatThrows()
} catch (e: Exception) {
Measure.trackHandledException(e)
}
```
### Add attributes \[#add-attributes]
Attach attributes to a handled error to record additional context about the error. You can filter and group by these attributes on the dashboard. See [Attribute limits](https://measure.sh/docs/api-reference#attribute-limits) for the allowed keys and values.
```kotlin
import sh.measure.android.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Login").build()
Measure.trackHandledException(e, attributes)
```
```swift
Measure.trackError(error, attributes: ["screen": .string("Login")])
```
```dart
final attributes = AttributeBuilder().add("screen", "Login").build();
Measure.instance.trackHandledError(e, stackTrace, attributes: attributes);
```
```typescript
Measure.trackError({ error: e, attributes: { screen: "Login" } });
```
```kotlin
import sh.measure.kmp.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Login").build()
Measure.trackHandledException(e, attributes)
```
## Session replays \[#session-replays]
Measure collects a session replay with every **fatal** and **unhandled** error. It's an ordered list of what happened leading up to the error: the screens opened, taps, network calls, logs, app lifecycle events, network changes and more. Use it to retrace exactly what the user did before it broke.
By default the replay covers the 5 minutes before the error. A longer window gives more context but collects more data. Change it from the dashboard through [Adaptive Capture](https://measure.sh/docs/adaptive-capture#session-replay-duration).
## Screenshots \[#screenshots]
Each crash and ANR includes a screenshot of what was on screen when the issue occurred. Sensitive content is masked on the device before the screenshot is uploaded, and the [screenshot mask level](https://measure.sh/docs/adaptive-capture#screenshot-mask-level) controls how much is hidden. See [Screenshot masking](https://measure.sh/docs/error-monitoring/screenshot-masking) for how masking works on each platform and the APIs to mask specific views.
Screenshots for crashes aren't supported on iOS.
## Alerts \[#alerts]
A spike of crashes or ANRs triggers an alert over email and Slack. Handled errors never trigger alerts. See [Alerts](https://measure.sh/docs/alerts) for how spikes are detected, how to tune the thresholds, and how to set up delivery.
## Configuration options \[#configuration-options]
Crashes and ANRs are configured remotely through [Adaptive Capture](https://measure.sh/docs/adaptive-capture), so you can change these without shipping a new build:
* **Screenshots**: whether Measure captures a screenshot on each crash and ANR. On by default.
* **Session replay duration**: how far back the replay reaches before the error. A longer window gives more context but collects more data. 5 minutes by default.
* **Screenshot mask level**: how much of each screenshot is masked to keep sensitive content from leaking. Masks all text and media by default.
See [Errors](https://measure.sh/docs/adaptive-capture#errors) for each setting.
---
Source: https://measure.sh/docs/error-monitoring/screenshot-masking
---
# Screenshot masking
Mask sensitive content in screenshots on the device before they're uploaded, with APIs for masking specific views.
Each crash, ANR, and bug report includes an optional screenshot. Sensitive content is masked on the device before the screenshot is uploaded, so it never reaches the server. How much gets hidden is set by the [screenshot mask level](https://measure.sh/docs/adaptive-capture#screenshot-mask-level), configured from the dashboard.
What masking can detect on its own differs by platform.
## Android \[#android]
Masking requires no setup. The SDK automatically inspects the view hierarchy, both Views and Jetpack Compose, and hides content based on the selected mask level.
## iOS \[#ios]
UIKit requires no setup. Every visible element is a `UIView`, and masking traverses the view hierarchy to redact sensitive types like `UILabel`, `UITextField`, and `UITextView` based on the mask level.
SwiftUI works differently. Most SwiftUI views don't produce individual `UIView` instances; the whole tree renders inside a single hosting view that can't be inspected view by view. The entire hosting view is masked by default instead, whatever the mask level, so no SwiftUI content leaks. Two modifiers control individual views.
### Mask a SwiftUI view \[#mask-a-swiftui-view]
`.msrMask()` marks a view as sensitive, redacting its frame whether or not automatic detection would catch it. Use it for standalone `Text` views and custom views holding user data.
```swift
Text(user.email)
.msrMask()
```
### Unmask a SwiftUI view \[#unmask-a-swiftui-view]
`.msrUnmask()` reveals a view that isn't sensitive, like a navigation title, an icon, or a static label. The view's frame is cut out of the masked region.
```swift
Text("Welcome back")
.msrUnmask()
```
Combine the two in screens that mix sensitive and non-sensitive content. Fields backed by UIKit inputs, like `TextField` and `SecureField`, are masked automatically.
```swift
Form {
Section("Profile") {
Text("Username")
.msrUnmask()
TextField("Username", text: $username)
}
Section("Security") {
SecureField("Password", text: $password)
Text(recoveryHint)
.msrMask()
}
}
```
## Flutter \[#flutter]
The SDK walks the widget tree under the `MeasureWidget` that wraps your app and redacts detected widgets based on the selected mask level. Automatic detection covers:
* `Text` and `RichText`
* `TextField` and `EditableText`
* `Image`
A `TextField` with `obscureText` or a password, email, or phone keyboard type is always masked, whatever the mask level. Text inside buttons, `InkWell`, or a `GestureDetector` with handlers counts as clickable, so the "mask text except clickable" level leaves it visible.
Custom-drawn content isn't detected. Wrap it with `MsrMask` to always redact its area, regardless of the mask level.
```dart
MsrMask(
child: AccountBalance(amount: balance),
)
```
## React Native \[#react-native]
Masking requires no setup. React Native renders native views, so the view hierarchy inspection from Android and iOS applies as is.
## Kotlin Multiplatform \[#kotlin-multiplatform]
Masking requires no setup. Kotlin Multiplatform runs the native SDKs, so masking works the same as Android and iOS.
---
Source: https://measure.sh/docs/error-monitoring/upload-symbols
---
# Upload symbols
Upload ProGuard/R8 mapping files, dSYM files, Dart symbol files and JavaScript sourcemaps to symbolicate stack traces in Android, iOS, Flutter and React Native apps.
When an error is reported, its stack trace often comes back obfuscated or full of bare memory addresses. Upload your app's mapping or symbol files and Measure turns them back into readable stack traces. Which files you need depends on your platform.
## Android \[#android]
If you use ProGuard or R8 to obfuscate your code, Measure needs the mapping files to de-obfuscate stack traces. Measure's Android Gradle Plugin, added in [getting started](https://measure.sh/docs/getting-started/android#3-add-the-gradle-plugin), uploads them automatically when you run an `assemble` Gradle task, so there's nothing else to do.
## iOS \[#ios]
On iOS, a crash report comes back as bare memory addresses until your dSYM files decode it. Upload the dSYMs with a shell script or straight from an XCArchive, whichever fits your build.
### Using a shell script \[#using-a-shell-script]
Run the [`upload_dsym_manual.sh`](https://github.com/measure-sh/measure/blob/main/ios/Scripts/upload_dsym_manual.sh) script to manually upload dSYM files after building your app.
```sh
./upload_dsym_manual.sh [custom_headers]
```
### Using XCArchive \[#using-xcarchive]
The [`upload_dsym_xcarchive.sh`](https://github.com/measure-sh/measure/blob/main/ios/Scripts/upload_dsym_xcarchive.sh) script pulls the dSYMs from your `.xcarchive` and uploads them automatically.
```sh
./upload_dsym_xcarchive.sh [custom_headers] [ipa_path]
```
## React Native \[#react-native]
A React Native crash surfaces as a JavaScript stack trace, and Measure needs the matching sourcemap to make sense of it. How you get that sourcemap uploaded differs a little between Android and iOS.
### Android \[#android-1]
On Android, the Measure Android Gradle Plugin handles this for you: it captures and uploads the composed JavaScript sourcemap whenever you run `assembleRelease` or `bundleRelease`, so there's nothing more to configure. See [getting started](https://measure.sh/docs/getting-started/android#3-add-the-gradle-plugin) to add it or check its version.
### iOS \[#ios-1]
Add `upload_build_phase.sh` as a Run Script Build Phase in Xcode and it uploads your dSYM files and JavaScript sourcemaps together on every build.
**Step 1: Enable sourcemap generation**
In Xcode, open your target → Build Phases → \*\*"Bundle React Native code and images"\*\* and add this line at the top of the script:
```sh
export SOURCEMAP_FILE="$(pwd)/main.jsbundle.map"
```
**Step 2: Add the upload build phase**
In Xcode, add a new Run Script Build Phase **after** the "Bundle React Native code and images" phase with the following content:
```sh
"${SRCROOT}/../node_modules/@measuresh/react-native/scripts/upload_build_phase.sh" \
"https://your-api-url.measure.sh" \
"your-api-key"
```
Replace the API URL and API key with your values from the Measure dashboard.
The script runs on every build in whatever configuration you add it to. To restrict it to Archive builds only, wrap the script body in:
```sh
if [ "$ACTION" = "archive" ]; then
# script content here
fi
```
## Flutter \[#flutter]
Obfuscating a Flutter app with `--obfuscate` and `--split-debug-info` scrambles its stack traces, so Measure needs the generated symbol files to read them. Where those files come from depends on the platform:
* **Android**: The Measure Android Gradle Plugin automatically uploads the required mapping files.
* **iOS**: `flutter build ipa` produces an Xcode archive at `build/ios/archive/Runner.xcarchive`. Upload its dSYMs with the [`upload_dsym_xcarchive.sh`](https://github.com/measure-sh/measure/blob/main/ios/Scripts/upload_dsym_xcarchive.sh) script:
```sh
./upload_dsym_xcarchive.sh \
build/ios/archive/Runner.xcarchive \
\
```
---
Source: https://measure.sh/docs/error-monitoring/grouping
---
# Grouping
See how Measure groups similar errors into a single issue.
Similar errors are grouped into a single issue. Each group shows its percentage contribution so you can see at a glance what to fix first.
Grouping rules depend on the platform.
## Android \[#android]
Errors are grouped by the exception type and the top frame of the stack trace.
* **Exception type**: `java.lang.NullPointerException`
* **Top frame**: `onCreate` in `MainActivity.kt`
```log
java.lang.NullPointerException: Attempt to invoke virtual method 'void com.example.app.MainActivity.onCreate(android.os.Bundle)' on a null object reference
at com.example.app.MainActivity.onCreate(MainActivity.kt:10)
at android.app.Activity.performCreate(Activity.java:8000)
...
```
## iOS \[#ios]
Errors are grouped by the signal and the first stack frame that belongs to your app. Operating-system and framework frames at the top of the stack (like `CoreFoundation` and `libobjc`) are skipped, since the code you can fix is in your own binary.
* **Signal**: `SIGABRT`
* **First frame from your app**: `-viewDidLoad` in `MainViewController.m`
```log
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Crashed Thread: 0
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MainViewController viewDidLoad]: unrecognized selector sent to instance 0x600000e2c0c0'
First Throw Call Stack:
(
0 CoreFoundation 0x000000010a2f3b6c __exceptionPreprocess + 220
1 libobjc.A.dylib 0x0000000109d8e5e1 objc_exception_throw + 48
2 MainViewController.m 0x000000010a2f3b6c -[MainViewController viewDidLoad] + 0
...
)
```
## Flutter \[#flutter]
Errors are grouped by the exception type and the top frame of the stack trace.
* **Exception type**: `FlutterError`
* **Top frame**: `_incrementCounter` in `main.dart`
```log
FlutterError (setState() called after dispose(): _MyHomePageState#12345(ticker: _TickerModeEnabled))
at _MyHomePageState._incrementCounter (package:my_app/main.dart:42:9)
at _MyHomePageState.build (package:my_app/main.dart:30:5)
at StatelessElement.build (package:flutter/src/widgets/framework.dart:4620:27)
...
```
## React Native \[#react-native]
Errors are grouped by the error type, its message, and the top frame of the stack trace.
* **Error type**: `TypeError`
* **Message**: `undefined is not an object (evaluating 'user.profile.name')`
* **Top frame**: `renderHeader` in `ProfileScreen.js`
```log
TypeError: undefined is not an object (evaluating 'user.profile.name')
at renderHeader (ProfileScreen.js:42:18)
at ProfileScreen (ProfileScreen.js:15:10)
...
```
## Kotlin Multiplatform \[#kotlin-multiplatform]
Errors follow the host platform's grouping: Android rules on the Android target and iOS rules on the iOS target, including crashes in shared Kotlin code.
---
Source: https://measure.sh/docs/error-monitoring/over-the-air-updates
---
# Over-the-air updates
Symbolicate crashes in over-the-air updates by uploading the matching symbols for each patch.
Over-the-air (OTA) updates let you ship changes without a full native app release. When a crash happens in a patched bundle, Measure needs the matching sourcemap to symbolicate the stack trace.
## React Native \[#react-native]
Each OTA update needs its own sourcemap, tied to a patch ID so Measure can match a crash to the exact bundle it came from. How you set that patch ID depends on whether you're on Expo.
### Set a patch version \[#set-a-patch-version]
Alongside the patch ID, you can attach an optional patch version, a human-readable label for the OTA update. The patch ID is a UUID Measure uses internally to match a crash to its sourcemap; the patch version is what you read in the dashboard to tell one release apart from another.
Set it in two places for each OTA update, using the same value:
* In `MeasureConfig` via the `patchVersion` field, so every event and crash from the patched bundle is tagged with it.
* In the `upload_patch.sh` script via the `--patch_version` flag, so the uploaded sourcemap carries the same label.
```typescript
new MeasureConfig({
patchVersion: "v1.2.3-hotfix",
})
```
On Expo, set the patch version to the same message you pass to `eas update --message`. Using one string in both places lets you match a release in Expo to the same release in Measure at a glance.
```sh
eas update --branch production --message "v1.2.3-hotfix"
```
### Expo \[#expo]
The Metro plugin sets the patch ID for you. Export with source maps and upload them after every deploy.
**Step 1: Add `withMeasureConfig` to `metro.config.js`**
The plugin injects a patch ID into each bundle at build time, with no changes to your SDK initialization code.
```js
const { getDefaultConfig } = require('expo/metro-config');
const { withMeasureConfig } = require('@measuresh/react-native/metro');
const config = getDefaultConfig(__dirname);
module.exports = withMeasureConfig(config);
```
**Step 2: Export with source maps**
```sh
npx expo export --platform all --source-maps --output-dir dist
```
The export produces `.hbc.map` files under `dist/_expo/static/js/ios/` and `dist/_expo/static/js/android/`.
**Step 3: Upload the sourcemaps**
Run `upload_patch.sh` after deploying each OTA update. The plugin already put the patch ID in the bundle, so the script reads it straight from the sourcemap. The script ships with the SDK package.
```sh
./node_modules/@measuresh/react-native/scripts/upload_patch.sh \
--api_key "your-api-key" \
--api_url "https://your-measure-url" \
--path_to_sourcemap "./dist/_expo/static/js/ios/entry-abc123.hbc.map" \
--patch_version "v1.2.3-hotfix"
```
### Manual \[#manual]
Without the Metro plugin, generate the patch ID yourself and pass it to `MeasureConfig`.
**Step 1: Generate a patch ID**
Generate a UUID v4 for the patch. Use any UUID library, or the shell command below:
```sh
uuidgen | tr '[:upper:]' '[:lower:]'
```
**Step 2: Pass the patch ID to `MeasureConfig`**
```typescript
import { Measure, MeasureConfig } from '@measuresh/react-native';
Measure.init({
config: new MeasureConfig({
autoStart: true,
patchId: 'your-patch-uuid',
patchVersion: 'v1.2.3-hotfix',
}),
});
```
The `patchId` field must match the UUID you pass to the upload script.
**Step 3: Generate the sourcemap**
Generate the JavaScript sourcemap when you build the OTA bundle. The exact command depends on your OTA provider. For a manual React Native bundle:
```sh
npx react-native bundle \
--platform ios \
--dev false \
--entry-file index.js \
--bundle-output main.jsbundle \
--sourcemap-output main.jsbundle.map
```
**Step 4: Upload the sourcemap**
Pass the sourcemap and the patch ID you set to `upload_patch.sh`:
```sh
./node_modules/@measuresh/react-native/scripts/upload_patch.sh \
--api_key "your-api-key" \
--api_url "https://your-measure-url" \
--path_to_sourcemap "./path/to/main.jsbundle.map" \
--patch_id "your-patch-uuid" \
--patch_version "v1.2.3-hotfix"
```
## Flutter \[#flutter]
Flutter ships over-the-air updates through [Shorebird](https://shorebird.dev). Support for symbolicating Shorebird patches is coming soon.
---
Source: https://measure.sh/docs/network-monitoring
---
# Network Monitoring
Monitor HTTP requests, responses, and failures across Android, iOS, Flutter, React Native, and Kotlin Multiplatform, with status codes, latency, and per-endpoint metrics.
## Overview \[#overview]
Measure captures your app's network requests, responses, and failures, so you can see how your APIs are performing.
Each request records its URL, HTTP method, status code, latency and failure reason. To show aggregated metrics URLs are grouped into [endpoint patterns](https://measure.sh/docs/network-monitoring/endpoint-patterns) automatically.
Request and response headers and bodies are collected only for the URLs you opt into, so sensitive data stays out by default. Choose those URLs from the dashboard through the [HTTP events](https://measure.sh/docs/adaptive-capture#http-events) settings.
Beyond HTTP, Measure records [connectivity changes](https://measure.sh/docs/network-monitoring/connectivity-changes) so you can tell when the network itself was the problem.
## Tracked automatically \[#tracked-automatically]
Requests from the clients below are captured automatically.
### Android \[#android]
Add the [Measure Android Gradle Plugin](https://measure.sh/docs/getting-started/android#3-add-the-gradle-plugin) to enable tracking. Requests from **OkHttp** (versions `4.7.0` to `5.3.2`) and **HttpURLConnection** (SDK `0.18.0` and later) are tracked automatically.
### iOS \[#ios]
Requests from **URLSession** are tracked automatically, including any request made by a third-party library using it.
### Kotlin Multiplatform \[#kotlin-multiplatform]
Requests from **Ktor** are tracked when it runs on the OkHttp (Android) or Darwin (iOS) engine. A different engine like CIO isn't tracked automatically. You can [track those manually](#track-a-request-manually).
## Add an interceptor \[#add-an-interceptor]
A few clients need a one-time setup so that their requests can be tracked.
### Dio (Flutter) \[#dio-flutter]
Add the `measure_dio` package and register `MsrInterceptor` on your `Dio` instance:
```yaml
dependencies:
measure_dio: latest-version
```
```dart
final dio = Dio();
dio.interceptors.add(MsrInterceptor());
```
For Flutter's `http` package or any other client, [track requests manually](#track-a-request-manually).
### Retrofit (Android) \[#retrofit-android]
Retrofit runs on OkHttp, but pulls it in as a transitive dependency, and auto-instrumentation doesn't reach transitive dependencies. If your project depends on Retrofit alone, use one of the following ways to track requests.
**Declare OkHttp as a direct dependency.** Pin an OkHttp version in your `build.gradle.kts`. Auto-instrumentation then works with no code changes:
```kotlin
dependencies {
implementation("com.squareup.retrofit2:retrofit:")
implementation("com.squareup.okhttp3:okhttp:")
}
```
**Add the interceptor manually.** Build the `OkHttpClient` used by Retrofit with Measure's interceptor and event listener factory:
```kotlin
val client = OkHttpClient.Builder()
.addInterceptor(MeasureOkHttpApplicationInterceptor())
.eventListenerFactory(MeasureEventListenerFactory(null))
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(client)
.build()
```
### React Native \[#react-native]
#### Android \[#android-1]
Configure a custom `OkHttpClient` with `OkHttpClientProvider` in `MainApplication.kt`. Without this, HTTP events aren't tracked on Android:
```kotlin
OkHttpClientProvider.setOkHttpClientFactory(object : OkHttpClientFactory {
override fun createNewNetworkModuleClient(): OkHttpClient {
return OkHttpClient.Builder()
.cookieJar(ReactCookieJarContainer())
.addInterceptor(MeasureOkHttpApplicationInterceptor())
.eventListenerFactory(MeasureEventListenerFactory(null))
.build()
}
})
```
#### iOS \[#ios-1]
iOS tracks requests automatically. However if you want to capture response body as well (configurable on dashboard) then add `MSRNetworkInterceptor` to the session configuration in `AppDelegate.mm`:
```objc
RCTSetCustomNSURLSessionConfigurationProvider(^NSURLSessionConfiguration *{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
[MSRNetworkInterceptor enableOn:configuration];
return configuration;
});
```
Response bodies are recorded only for the URLs you opt into, set with the **Track HTTP response body for URLs** option in [Adaptive Capture](https://measure.sh/docs/adaptive-capture#http-events).
For clients other than `fetch` and `XHR` follow [track requests manually](#track-a-request-manually).
## Track a request manually \[#track-a-request-manually]
For any HTTP clients not instrumented automatically, record each request yourself with `trackHttpEvent`. Use `getCurrentTime` for the start and end time, so the duration stays right even if the device clock shifts.
```kotlin
import sh.measure.android.Measure
val startTime = Measure.getCurrentTime()
// make the request
val endTime = Measure.getCurrentTime()
Measure.trackHttpEvent(
url = "https://api.example.com/users",
method = "GET",
startTime = startTime,
endTime = endTime,
statusCode = 200,
)
```
```swift
import Measure
let startTime = UInt64(Measure.getCurrentTime())
// make the request
let endTime = UInt64(Measure.getCurrentTime())
Measure.trackHttpEvent(
url: "https://api.example.com/users",
method: "GET",
startTime: startTime,
endTime: endTime,
statusCode: 200
)
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final startTime = Measure.instance.getCurrentTime();
// make the request
final endTime = Measure.instance.getCurrentTime();
Measure.instance.trackHttpEvent(
url: "https://api.example.com/users",
method: HttpMethod.get,
startTime: startTime,
endTime: endTime,
statusCode: 200,
);
```
```typescript
import { Measure } from '@measuresh/react-native';
const startTime = Measure.getCurrentTime();
// make the request
const endTime = Measure.getCurrentTime();
Measure.trackHttpEvent({
url: "https://api.example.com/users",
method: "GET",
startTime,
endTime,
statusCode: 200,
});
```
```kotlin
import sh.measure.kmp.Measure
val startTime = Measure.getCurrentTime()
// make the request
val endTime = Measure.getCurrentTime()
Measure.trackHttpEvent(
url = "https://api.example.com/users",
method = "GET",
startTime = startTime,
endTime = endTime,
statusCode = 200,
)
```
## Configuration options \[#configuration-options]
Control what network data is collected from the dashboard using [Adaptive Capture](https://measure.sh/docs/adaptive-capture). This allows changing configuration
without making an app release.
* **Sampling rate**: the fraction of HTTP events collected. All of them are collected by default.
* **Collect or ignore URLs**: turn collection on or off for specific URLs.
* **Request body collection**: the URLs whose request bodies and headers are recorded. Off by default.
* **Response body collection**: the URLs whose response bodies and headers are recorded. Off by default.
* **Blocked headers**: headers to never collect. `Authorization`, `Cookie` and more sensitive headers are always blocked.
See [HTTP events](https://measure.sh/docs/adaptive-capture#http-events) for details on each setting.
---
Source: https://measure.sh/docs/network-monitoring/endpoint-patterns
---
# Endpoint patterns
See how Measure groups similar request URLs into endpoint patterns, when a new endpoint shows up, and how to search for one on the dashboard.
Measure groups many similar request URLs into one **endpoint pattern**. Requests to `/api/users/123/profile` and `/api/users/456/profile` roll up into `/api/users/*/profile`. You can view combined latency, error rate, and volume for that endpoint. This happens automatically.
## How a URL becomes a pattern \[#how-a-url-becomes-a-pattern]
Measure wildcards the parts of a path that change from request to request. A segment gets replaced with `*` in one of two ways.
### Known dynamic formats \[#known-dynamic-formats]
Measure matches each segment against the formats that dynamic values usually take, and replaces a match with `*`. A segment is replaced when it's:
* A UUID (e.g. `550e8400-e29b-41d4-a716-446655440000`).
* A SHA-1 or MD5 hash.
* An ISO 8601 date-time, the `2024-01-15T...` form.
* A `0x`-prefixed hex value (e.g. `0x1a2b3c`).
* Any segment with two or more digits in a row. Single-digit segments like `v1` are kept.
So `/api/users/550e8400-e29b-41d4-a716-446655440000/orders/12345` becomes `/api/users/*/orders/*`.
### High-cardinality segments \[#high-cardinality-segments]
Some changing segments don't match any known format, like a product slug or a username. Measure tracks how many distinct values appear at each position in a path. Once a position collects many distinct values (more than 10), Measure collapses it to `*` too. So `/api/products/aluminium`, `/api/products/copper`, and dozens more roll up into `/api/products/*`.
A pattern appears once its endpoint gets more than 50 requests in an hour.
A brand-new or low-traffic endpoint won't show up in [Network Monitoring](https://measure.sh/docs/network-monitoring) right away.
## Searching for endpoints \[#searching-for-endpoints]
Search for an endpoint or explore a group of related endpoints. Type a domain, path, or keyword, then select a matching result to view its charts.
| Search | Example |
| ---------------------------------------- | --------------------------------- |
| Free text search | Enter any text like `users` |
| Domain | `api.example.com` |
| Everything below a path | `api.example.com/v1/` |
| A specific endpoint | `api.example.com/v1/products/123` |
| Routes one level below a path | `api.example.com/v1/*` |
| All routes below a path, nested included | `api.example.com/v1/**` |
Search is not case-sensitive. You can also use wildcards to explore a group of endpoints:
* Use `*` for one part of a URL path. For example, `/v1/*` matches `/v1/cart`, but not `/v1/products/123`.
* Use `**` at the end of a path to include all nested routes. For example, `/v1/**` includes both `/v1/cart` and `/v1/products/123`.
## Top endpoints \[#top-endpoints]
The top endpoints section provides a ranked summary of your app's API endpoints across three dimensions.
| Ranking | Ordered by |
| ------------------- | ------------------------------------------------------------------------- |
| **Slowest** | p95 latency, helping identify the slowest APIs affecting user experience. |
| **Highest error %** | Percentage of 4xx and 5xx responses, highlighting unreliable APIs. |
| **Most frequent** | Total request count, showing which APIs are called most often. |
Selecting an endpoint opens a detail view with latency percentiles (p50, p90, p95, p99) and status codes over time.

## Request timeline \[#request-timeline]
The request timeline is a heatmap of when requests happen relative to session start. It shows the top endpoint patterns ranked by request frequency, and helps answer questions like "which APIs fire on app launch?" and "is anything called repeatedly in the background?"

Requires SDK version 0.16.2 on Android and 0.9.2 on iOS.
---
Source: https://measure.sh/docs/network-monitoring/connectivity-changes
---
# Connectivity changes
Track when your app gains or loses connectivity, switches between WiFi and cellular, or changes carrier.
Measure records changes to the device's network state. It captures the connection type (WiFi, cellular, and so on), the network provider (Airtel, T-Mobile), and the network generation (2G, 3G, 4G). Tracking is automatic once the SDK is in place. What it captures depends on the platform and the permissions your app already holds.
## Android \[#android]
Connectivity tracking turns on only when your app holds the `ACCESS_NETWORK_STATE` permission and runs on Android M (API 23) or higher. Measure adds no permissions of its own, so it works with what your app already has.
The network generation is captured for cellular connections, and needs the [`READ_PHONE_STATE`](https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE) runtime permission. If the user denies it, the generation isn't recorded. On Android Tiramisu (API 33) and later, [`READ_BASIC_PHONE_STATE`](https://developer.android.com/reference/android/Manifest.permission#READ_BASIC_PHONE_STATE) is enough and doesn't require runtime permission from users.
## iOS \[#ios]
Connectivity changes are captured automatically for versions that support it.
Since iOS 16.4, `network_provider` is no longer available from the OS.
## Flutter and React Native \[#flutter-and-react-native]
On Flutter and React Native, connectivity tracking runs on the native Android and iOS behavior above, so the same permissions and limits apply.
---
Source: https://measure.sh/docs/bug-reports
---
# Bug Reporting
Let users report bugs from inside your app with screenshots.
Measure lets users report bugs from inside your app, so you hear about the problems directly from customers.
Each report can hold up to five screenshots and a description of up to 4,000 characters entered by the user. Screenshots use the same [screenshot mask level](https://measure.sh/docs/adaptive-capture#screenshot-mask-level) as the rest of Measure, so sensitive content stays out.
## Launch the bug report screen \[#launch-the-bug-report-screen]
Every SDK ships a native bug report UI. It captures the current screen automatically when triggered, and users add more before they send. Launch it with a single call:
```kotlin
import sh.measure.android.Measure
Measure.launchBugReportActivity(takeScreenshot = true)
```
```swift
import Measure
Measure.launchBugReport(takeScreenshot: true)
```
The bug report screen is a widget, typically pushed as a new route:
```dart
import 'package:flutter/material.dart';
import 'package:measure_flutter/measure_flutter.dart';
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Measure.instance.createBugReportWidget(),
),
);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.launchBugReport({ takeScreenshot: true });
```
```kotlin
import sh.measure.kmp.Measure
Measure.launchBugReport(takeScreenshot = true)
```
On Android and iOS, you can [theme the screen](https://measure.sh/docs/bug-reports/theming) to match your app.
| Dark mode | Light mode |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
|
|
| Dark mode | Light mode |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
|
|
| Dark mode | Light mode |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
|
|
React Native runs on the same native SDKs, so the screen matches the Android and iOS tabs.
Kotlin Multiplatform runs on the same native SDKs, so the screen matches the Android and iOS tabs.
## Shake to report \[#shake-to-report]
Users can also open a bug report by shaking their device, from anywhere in the app. Register a shake handler to enable it:
```kotlin
import sh.measure.android.Measure
import sh.measure.android.bugreport.MsrShakeListener
Measure.setShakeListener(object : MsrShakeListener {
override fun onShake() {
Measure.launchBugReportActivity()
}
})
```
```swift
import Measure
Measure.onShake {
Measure.launchBugReport()
}
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.setShakeListener(() {
// open the bug report widget
});
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.onShake({ handler: () => {
Measure.launchBugReport();
} });
```
Shake to report isn't part of Kotlin Multiplatform's shared API.
Pass a null handler to turn shake to report off again.
## Session replay \[#session-replay]
Each bug report comes with a session replay. It's an ordered list of what happened leading up to the bug report: the screens opened, taps, network calls, errors, and logs. Use it to retrace exactly what the user did before reporting the bug.
By default the replay covers the 5 minutes before the report. Change how far back it reaches from the dashboard through [Adaptive Capture](https://measure.sh/docs/adaptive-capture#bug-reports).
## Custom attributes \[#custom-attributes]
Attach custom attributes to a report to carry context that helps you triage, like the user's plan, an order ID, or the screen they were on. You can then filter and group reports by them in the dashboard. Attributes attach in either flow, whether you open the bug report UI or call `trackBugReport` from your own.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Cart").build()
Measure.launchBugReportActivity(takeScreenshot = true, attributes = attributes)
Measure.trackBugReport(description = "...", attributes = attributes)
```
```swift
import Measure
let attributes: [String: AttributeValue] = ["screen": .string("Cart")]
Measure.launchBugReport(takeScreenshot: true, attributes: attributes)
Measure.trackBugReport(description: "...", attributes: attributes)
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final attributes = AttributeBuilder().add("screen", "Cart").build();
Measure.instance.createBugReportWidget(attributes: attributes);
Measure.instance.trackBugReport(description: "...", attachments: [], attributes: attributes);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.launchBugReport({ takeScreenshot: true, attributes: { screen: "Cart" } });
Measure.trackBugReport({ description: "...", attributes: { screen: "Cart" } });
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Cart").build()
Measure.launchBugReport(takeScreenshot = true, attributes = attributes)
Measure.trackBugReport(description = "...", attributes = attributes)
```
See [Attribute limits](https://measure.sh/docs/api-reference#attribute-limits) for the allowed keys and values.
## Build your own UI \[#build-your-own-ui]
If you already have a bug reporting UI, or want to build one that matches your app, submit the report with `trackBugReport`. Descriptions can be up to 4,000 characters, with up to five attachments.
```kotlin
import sh.measure.android.Measure
Measure.trackBugReport(description = "Cart items disappear after reopening the app")
```
```swift
import Measure
Measure.trackBugReport(description: "Cart items disappear after reopening the app")
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.trackBugReport(
description: "Cart items disappear after reopening the app",
attachments: [],
attributes: {},
);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.trackBugReport({ description: "Cart items disappear after reopening the app" });
```
```kotlin
import sh.measure.kmp.Measure
Measure.trackBugReport(description = "Cart items disappear after reopening the app")
```
## Configuration \[#configuration]
Bug reports are configured remotely through [Adaptive Capture](https://measure.sh/docs/adaptive-capture), so you can change what's collected without shipping a new build. Set the session replay duration to control how far back the replay reaches before a report, 5 minutes by default. See [Bug reports](https://measure.sh/docs/adaptive-capture#bug-reports) for the setting.
---
Source: https://measure.sh/docs/bug-reports/theming
---
# Theming
Restyle the built-in bug report screen with custom colors, text sizes, and spacing on Android and iOS.
The built-in bug report UI ships with a default look, and on Android and iOS you can restyle it to match your app. Override its colors, text sizes, and spacing so the report screen feels like the rest of your product.
The screen uses the `Theme.MsrBugReport` theme, and its colors come from named color resources. Android resolves resources by name and lets your app override them. So redefining `msr_background`, `msr_text_primary`, or any of the others in your own `colors.xml` restyles the bug report screen.
Measure ships separate light and dark colors, so set the light values in `res/values/colors.xml` and the dark ones in `res/values-night/colors.xml`:
```xml
#FFFFFF
#101010
#101010
#FFFFFF
```
Text sizes, corner radius, and the other tokens live in the theme itself. See [themes.xml](https://github.com/measure-sh/measure/blob/main/android/measure-android/measure/src/main/res/values/themes.xml) for the full set, and Android's [theming guide](https://developer.android.com/develop/ui/views/theming/themes#CustomizeTheme) for how overriding works.
Pass a `BugReportConfig` to `launchBugReport`. Start from the defaults, override the colors you want with `update`, and adjust spacing with `MsrDimensions`:
```swift
let colors = BugReportConfig.default.colors.update(
darkBackground: UIColor(white: 0.1, alpha: 1),
darkText: .white,
badgeColor: .systemOrange,
isDarkMode: true
)
let config = BugReportConfig(colors: colors, dimensions: MsrDimensions(topPadding: 24))
Measure.launchBugReport(takeScreenshot: true, bugReportConfig: config)
```
Set `isDarkMode` to match the user's preference. `BugReportConfig` also carries `text` and `fonts`, so you can localize the labels and swap the fonts from the same object. See [BugReportConfig](https://github.com/measure-sh/measure/blob/main/ios/Sources/MeasureSDK/Swift/BugReport/BugReportConfig) for every token you can set.
---
Source: https://measure.sh/docs/performance-tracing
---
# Performance Tracing
Trace any operation in your app with nested spans and attributes across Android, iOS, Flutter, React Native, and Kotlin Multiplatform.
Tracing shows how long an operation takes and where the time goes inside it.
A **span** times a single piece of work, like an HTTP request, a database query, or a function. A **trace** groups related spans into one operation, from something small like an app launch to a whole journey like onboarding. Spans nest inside a trace to show how the work is structured.
This page covers the common operations. See [Track performance traces](https://measure.sh/docs/api-reference#track-performance-traces) for the full API.
## Time an operation \[#time-an-operation]
Wrap an operation in a span to time it. Start a span when the work begins and end it when it's done. For example: time a feed refresh, an image decode, or a database query. Then set the status to mark whether the work succeeded (`Ok`) or failed (`Error`) before you end the span.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.tracing.SpanStatus
val span = Measure.startSpan("refresh-feed")
val refreshed = feedRepository.refresh()
span.setStatus(if (refreshed) SpanStatus.Ok else SpanStatus.Error).end()
```
```swift
import Measure
let span = Measure.startSpan(name: "refresh-feed")
let refreshed = await feedRepository.refresh()
span.setStatus(refreshed ? .ok : .error).end()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("refresh-feed");
final refreshed = await feedRepository.refresh();
span.setStatus(refreshed ? SpanStatus.ok : SpanStatus.error).end();
```
```typescript
import { Measure, SpanStatus } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "refresh-feed" });
const refreshed = await feedRepository.refresh();
span.setStatus(refreshed ? SpanStatus.Ok : SpanStatus.Error).end();
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.tracing.SpanStatus
val span = Measure.startSpan("refresh-feed")
val refreshed = feedRepository.refresh()
span.setStatus(if (refreshed) SpanStatus.Ok else SpanStatus.Error).end()
```
## Break a flow into steps \[#break-a-flow-into-steps]
A single span records how long the whole flow took. Often you need a drill down into what steps made it slow. To understand that, set one span as the **parent** of another to nest them. For example: for a checkout flow, create a parent span for the whole flow with a child span for each step, like validating the cart and processing the payment.
```kotlin
import sh.measure.android.Measure
val checkout = Measure.startSpan("checkout")
val validate = Measure.startSpan("validate-cart").setParent(checkout)
cartService.validate(cart)
validate.end()
val payment = Measure.startSpan("process-payment").setParent(checkout)
paymentGateway.charge(cart)
payment.end()
checkout.end()
```
```swift
import Measure
let checkout = Measure.startSpan(name: "checkout")
let validate = Measure.startSpan(name: "validate-cart").setParent(checkout)
await cartService.validate(cart)
validate.end()
let payment = Measure.startSpan(name: "process-payment").setParent(checkout)
await paymentGateway.charge(cart)
payment.end()
checkout.end()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final checkout = Measure.instance.startSpan("checkout");
final validate = Measure.instance.startSpan("validate-cart").setParent(checkout);
await cartService.validate(cart);
validate.end();
final payment = Measure.instance.startSpan("process-payment").setParent(checkout);
await paymentGateway.charge(cart);
payment.end();
checkout.end();
```
```typescript
import { Measure } from '@measuresh/react-native';
const checkout = Measure.startSpan({ name: "checkout" });
const validate = Measure.startSpan({ name: "validate-cart" }).setParent(checkout);
await cartService.validate(cart);
validate.end();
const payment = Measure.startSpan({ name: "process-payment" }).setParent(checkout);
await paymentGateway.charge(cart);
payment.end();
checkout.end();
```
```kotlin
import sh.measure.kmp.Measure
val checkout = Measure.startSpan("checkout")
val validate = Measure.startSpan("validate-cart").setParent(checkout)
cartService.validate(cart)
validate.end()
val payment = Measure.startSpan("process-payment").setParent(checkout)
paymentGateway.charge(cart)
payment.end()
checkout.end()
```
## Add context with attributes \[#add-context-with-attributes]
Attach attributes to a span to record additional context alongside its timing. For example, on a feed-load span, that might be the screen name, how many items are in the feed, or whether the data was served from cache. See [Attribute limits](https://measure.sh/docs/api-reference#attribute-limits) for the allowed keys and values.
```kotlin
import sh.measure.android.Measure
val span = Measure.startSpan("load-feed")
val items = feedApi.fetchFeed()
span.setAttribute("screen", "Home")
span.setAttribute("item_count", items.size)
span.end()
```
```swift
import Measure
let span = Measure.startSpan(name: "load-feed")
let items = await feedApi.fetchFeed()
span.setAttribute("screen", value: "Home")
span.setAttribute("item_count", value: items.count)
span.end()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("load-feed");
final items = await feedApi.fetchFeed();
span.setAttributeString("screen", "Home");
span.setAttributeInt("item_count", items.length);
span.end();
```
```typescript
import { Measure } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "load-feed" });
const items = await feedApi.fetchFeed();
span.setAttribute("screen", "Home");
span.setAttribute("item_count", items.length);
span.end();
```
```kotlin
import sh.measure.kmp.Measure
val span = Measure.startSpan("load-feed")
val items = feedApi.fetchFeed()
span.setAttribute("screen", "Home")
span.setAttribute("item_count", items.size)
span.end()
```
## Mark checkpoints \[#mark-checkpoints]
Mark a checkpoint inside a span to record a moment that matters. On a screen-load span, checkpoint when the network call returns and when the list finishes rendering. It helps to see which half of the load was slow. Checkpoints split one span's time into phases without a child span for each.
```kotlin
import sh.measure.android.Measure
val span = Measure.startSpan("load-screen")
val products = productApi.fetchProducts()
span.setCheckpoint("network_done")
renderList(products)
span.setCheckpoint("list_rendered")
span.end()
```
```swift
import Measure
let span = Measure.startSpan(name: "load-screen")
let products = await productApi.fetchProducts()
span.setCheckpoint("network_done")
renderList(products)
span.setCheckpoint("list_rendered")
span.end()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("load-screen");
final products = await productApi.fetchProducts();
span.setCheckpoint("network_done");
renderList(products);
span.setCheckpoint("list_rendered");
span.end();
```
```typescript
import { Measure } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "load-screen" });
const products = await productApi.fetchProducts();
span.setCheckpoint("network_done");
renderList(products);
span.setCheckpoint("list_rendered");
span.end();
```
```kotlin
import sh.measure.kmp.Measure
val span = Measure.startSpan("load-screen")
val products = productApi.fetchProducts()
span.setCheckpoint("network_done")
renderList(products)
span.setCheckpoint("list_rendered")
span.end()
```
## Defer or backdate a span \[#defer-or-backdate-a-span]
Some spans start before there's a good place to create them. A cold launch begins when the process starts but the SDK is not initialzied at the time. you can create a backdated span in such a case.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.tracing.SpanStatus
// in Application.onCreate
val launchStart = Measure.getCurrentTime()
// when the first screen is drawn
val span = Measure.startSpan("cold-launch", timestamp = launchStart)
span.setStatus(SpanStatus.Ok).end()
```
```swift
import Measure
// in application(_:didFinishLaunchingWithOptions:)
let launchStart = Measure.getCurrentTime()
// when the first screen appears
let span = Measure.startSpan(name: "cold-launch", timestamp: launchStart)
span.setStatus(.ok).end()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
// in main, before runApp
final launchStart = Measure.instance.getCurrentTime();
// when the first frame renders
final span = Measure.instance.startSpan("cold-launch", timestamp: launchStart);
span.setStatus(SpanStatus.ok).end();
```
```typescript
import { Measure, SpanStatus } from '@measuresh/react-native';
// in your entry file, before the app renders
const launchStart = Measure.getCurrentTime();
// when the first screen mounts
const span = Measure.startSpanWithTimestamp({ name: "cold-launch", timestampMs: launchStart });
span.setStatus(SpanStatus.Ok).end();
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.tracing.SpanStatus
// as early as possible in app startup
val launchStart = Measure.getCurrentTime()
// when the first screen is drawn
val span = Measure.startSpan("cold-launch", timestamp = launchStart)
span.setStatus(SpanStatus.Ok).end()
```
To configure a span now and start it at the right moment, build it in advance with `createSpanBuilder`.
```kotlin
import sh.measure.android.Measure
val builder = Measure.createSpanBuilder("checkout")
// later, when the work begins
val span = builder?.startSpan()
```
```swift
import Measure
let builder = Measure.createSpanBuilder(name: "checkout")
// later, when the work begins
let span = builder?.startSpan()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final builder = Measure.instance.createSpanBuilder("checkout");
// later, when the work begins
final span = builder?.startSpan();
```
```typescript
import { Measure } from '@measuresh/react-native';
const builder = Measure.createSpanBuilder({ name: "checkout" });
// later, when the work begins
const span = builder?.startSpan();
```
```kotlin
import sh.measure.kmp.Measure
val builder = Measure.createSpanBuilder("checkout")
// later, when the work begins
val span = builder?.startSpan()
```
## Control the sampling rate \[#control-the-sampling-rate]
Measure reports every trace by default. To collect less, set a [trace sampling rate](https://measure.sh/docs/adaptive-capture#trace-sampling) from the dashboard.
---
Source: https://measure.sh/docs/performance-tracing/screen-load-time
---
# Screen load time
Measure automatically traces how long Activities, Fragments, and ViewControllers take to draw their first frame, so you can find slow screens without any instrumentation.
Measure automatically traces how long an Activity or Fragment takes to load on Android, and a ViewController on iOS. A slow load means users wait too long to see content, so these traces show which screens to fix, with no instrumentation from you.
## Android \[#android]
**Activity load time** measures the time between the Activity being created and the first frame drawn on screen. This is also known as Time to First Frame or Time to Initial Display (TTID).
Each Activity load is captured with a span named `Activity TTID` followed by the fully qualified class name of the Activity. For `MainActivity`, the span name is `Activity TTID com.example.MainActivity`.
An attribute called `app_startup_first_activity` with a value of *true* is added when the Activity loaded as part of a cold launch.
**Fragment load time** measures the time between the Fragment view being created and the first frame drawn on screen, also known as Time to First Frame (TTF) or Time to Initial Display (TTID).
Each Fragment load is captured with a span named `Fragment TTID` followed by the fully qualified class name of the Fragment. For `HomeFragment`, the span name is `Fragment TTID com.example.HomeFragment`.
> The fully qualified name may be truncated to fit within the 64 character limit for span names.
### Data captured \[#data-captured]
A span named `Activity TTID {fully qualified activity name}` is created for each Activity load, and a span named `Fragment TTID {fully qualified fragment name}` for each Fragment load.
The Activity TTID span may carry the following attribute:
| Attribute | Description |
| ----------------------------- | ---------------------------------------------------------------- |
| app\_startup\_first\_activity | Whether this activity is the first activity launched in the app. |
The spans have the following checkpoints:
| Checkpoint | Description |
| ----------------------------- | ---------------------------------------- |
| fragment\_lifecycle\_attached | The time when the Fragment was attached. |
| fragment\_lifecycle\_started | The time when the Fragment was started. |
| fragment\_lifecycle\_resumed | The time when the Fragment was resumed. |
| activity\_lifecycle\_created | The time when the Activity was created. |
| activity\_lifecycle\_started | The time when the Activity was started. |
| activity\_lifecycle\_resumed | The time when the Activity was resumed. |
## iOS \[#ios]
**ViewController load time** measures the time between the ViewController's view being loaded and the first frame drawn on screen. This is also known as Time to First Frame or Time to Initial Display (TTID).
Each ViewController load is captured with a span named `VC TTID` followed by the fully qualified class name of the ViewController. For `MainViewController`, the span name is `VC TTID MainViewController`.
An attribute called `app_startup_first_view_controller` with a value of *true* is added when the ViewController loaded as part of a cold launch.
> The fully qualified name may be truncated to fit within the 64 character limit for span names.
### Data captured \[#data-captured-1]
A span named `VC TTID {view controller name}` is created for each ViewController load.
The ViewController TTID span may carry the following attribute:
| Attribute | Description |
| ------------------------------------- | ------------------------------------------------------------------ |
| app\_startup\_first\_view\_controller | Whether this view controller is the first one launched in the app. |
The spans have the following checkpoints:
| Checkpoint | Description |
| ---------------------- | ---------------------------------------------- |
| vc\_load\_view | The time when the view was loaded. |
| vc\_view\_did\_load | The time when the view was loaded into memory. |
| vc\_view\_will\_appear | The time when the view is about to appear. |
| vc\_view\_did\_appear | The time when the view is fully visible. |
---
Source: https://measure.sh/docs/performance-tracing/profiling
---
# Profiling
Auto-capture heap dumps and Perfetto traces from Android apps using the platform's trigger-based profiling APIs.
Profiling captures a Perfetto system trace or a heap dump at a specific moment, so you can see exactly what the app was doing when it launched slowly or froze.
Profiling is Android only. It needs Android 16 (API level 36) or higher, and is a no-op on older versions.
## Enable profiling \[#enable-profiling]
Profiling is off by default and turns on when [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager) is a dependency of your app. Measure needs it because a trace or heap dump is often over 10 MB and has to upload durably in the background, and it leaves WorkManager out of the SDK so apps that don't profile don't carry the dependency.
Add the [WorkManager dependency](https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started) to your `build.gradle.kts` to switch profiling on. Without it, the collector never registers and nothing is captured.
## Triggers \[#triggers]
A trigger is an occasion when the operating system may capture a profile. Measure registers two.
Android 17 (API level 37) adds more triggers, such as out-of-memory, cold start, and excessive CPU usage. Capturing them needs building against `compileSdk 37`, so Measure registers only the two below.
### App fully drawn (`app_fully_drawn`) \[#app-fully-drawn-app\_fully\_drawn]
Fires once the app reports that its first meaningful content is on screen. This one isn't automatic: call [`Activity.reportFullyDrawn()`](https://developer.android.com/reference/android/app/Activity#reportFullyDrawn\(\)) yourself once the first screen is ready, including any data loaded asynchronously.
```kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.loadHome()
}
// Call once the first meaningful content has finished rendering.
private fun onHomeContentReady() {
reportFullyDrawn()
}
}
```
### ANR (`anr`) \[#anr-anr]
Captured when the app stops responding. The operating system detects ANRs itself, so there's nothing to add in your code.
The OS rate-limits each trigger, and Measure caps it at one profile per trigger type per hour, so a repeating problem won't flood the session replay with captures.
## Sampling \[#sampling]
Profiling is sampled on its own, separate from session sampling. The rate runs from 0 to 100 and defaults to 100, so every profile the OS produces is kept. Keep it high: the OS already rate-limits each trigger to once an hour, so a low client-side rate would collect almost nothing. Set it well above your other sampling rates.
## How it works \[#how-it-works]
Measure uses [`android.os.ProfilingManager`](https://developer.android.com/reference/android/os/ProfilingManager), the trigger-based profiling API added in Android 16. The SDK registers the triggers above with the operating system, along with a callback for the results. The OS decides when to run a session, writes the result to a file, and hands it back, and Measure attaches that file to a `profile` event tagged with the trigger that produced it.
The artifacts are large, so a dedicated WorkManager worker uploads them in the background once the device has a network, apart from the normal event flow. After upload, you can download a result from the session replay in the dashboard.
## Further reading \[#further-reading]
* [Android ProfilingManager](https://developer.android.com/reference/android/os/ProfilingManager)
* [Android ProfilingTrigger](https://developer.android.com/reference/android/os/ProfilingTrigger)
* [Perfetto](https://perfetto.dev/)
---
Source: https://measure.sh/docs/custom-events
---
# Custom Events
Track app-specific events like user actions and feature usage, and view them in the session replay.
Custom events add app-specific context to the automatically collected data: user actions, feature usage, feature flags, or anything else worth seeing while debugging. They appear in the [session replay](https://measure.sh/docs/session-replay) with the rest of the session's events.
Track an event with `trackEvent`:
```kotlin
import sh.measure.android.Measure
Measure.trackEvent("checkout_completed")
```
```swift
import Measure
Measure.trackEvent(name: "checkout_completed", attributes: [:])
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.trackEvent(name: "checkout_completed");
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.trackEvent({ name: "checkout_completed" });
```
```kotlin
import sh.measure.kmp.Measure
Measure.trackEvent(name = "checkout_completed")
```
Events also accept attributes and a custom timestamp. See [Track custom events](https://measure.sh/docs/api-reference#track-custom-events) in the API reference for examples and naming rules.
Screen views and handled errors have dedicated methods, [`trackScreenView`](https://measure.sh/docs/api-reference#track-screen-views) and [`trackError`](https://measure.sh/docs/api-reference#track-errors). Prefer them over a custom event so these show up consistently across the dashboard.
---
Source: https://measure.sh/docs/logs
---
# Logging
Track logs with a severity level, add attributes, and control log collection from the dashboard.
Logs record a plain text message at a severity level. A log is free-form, best for context you'll read back while debugging rather than query precisely. Track one yourself, or turn on automatic collection to capture the logs you already write without touching each call site.
## Track a log \[#track-a-log]
Record a message at one of five severities, from debug through fatal, with `logDebug`, `logInfo`, `logWarning`, `logError`, and `logFatal`. The severity is what you filter and sort by in the dashboard, so pick the one that matches how much the message should stand out. Bodies longer than 1000 characters are truncated, so lead with the detail you'll search for.
```kotlin
import sh.measure.android.Measure
Measure.logDebug("Cache miss for key user_42")
Measure.logInfo("User signed in")
Measure.logWarning("Payment failed, retrying")
Measure.logError("Checkout request failed")
Measure.logFatal("Unrecoverable database error")
```
```swift
import Measure
Measure.logDebug("Cache miss for key user_42")
Measure.logInfo("User signed in")
Measure.logWarning("Payment failed, retrying")
Measure.logError("Checkout request failed")
Measure.logFatal("Unrecoverable database error")
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.logDebug("Cache miss for key user_42");
Measure.instance.logInfo("User signed in");
Measure.instance.logWarning("Payment failed, retrying");
Measure.instance.logError("Checkout request failed");
Measure.instance.logFatal("Unrecoverable database error");
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.logDebug({ body: "Cache miss for key user_42" });
Measure.logInfo({ body: "User signed in" });
Measure.logWarning({ body: "Payment failed, retrying" });
Measure.logError({ body: "Checkout request failed" });
Measure.logFatal({ body: "Unrecoverable database error" });
```
```kotlin
import sh.measure.kmp.Measure
Measure.logDebug("Cache miss for key user_42")
Measure.logInfo("User signed in")
Measure.logWarning("Payment failed, retrying")
Measure.logError("Checkout request failed")
Measure.logFatal("Unrecoverable database error")
```
A log is free-form text. If you're recording a named thing you'll want to search or chart later, like a user action or feature usage, a [custom event](https://measure.sh/docs/api-reference#track-custom-events) is easier to query. To capture an error with its stack trace and grouping, use the [handled error](https://measure.sh/docs/error-monitoring) APIs rather than logging it by hand.
## Add attributes \[#add-attributes]
Attach attributes to a log to record context you can filter on later, like the screen name, an order ID, or a retry count. The extra context is what lets you find one log among thousands.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Checkout").build()
Measure.logWarning("Payment failed", attributes)
```
```swift
import Measure
Measure.logWarning("Payment failed", attributes: ["screen": .string("Checkout")])
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final attributes = AttributeBuilder().add("screen", "Checkout").build();
Measure.instance.logWarning("Payment failed", attributes: attributes);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.logWarning({ body: "Payment failed", attributes: { screen: "Checkout" } });
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Checkout").build()
Measure.logWarning("Payment failed", attributes)
```
See [Attribute limits](https://measure.sh/docs/api-reference#attribute-limits) for the allowed keys and values.
## Collect logs automatically \[#collect-logs-automatically]
Measure can collect the logs your app already writes, so you don't have to route each one through the SDK by hand. It's off by default. Turn it on with the **Automatically collect logs** setting in the dashboard, which applies without a new release.
Automatic collection works on Android and React Native only. On iOS and Flutter, bridge your logging framework instead: forward [swift-log](https://measure.sh/docs/logs/ios) or the [`logging` package](https://measure.sh/docs/logs/flutter).
### Android \[#android]
Measure instruments `android.util.Log` at build time, so those calls flow to Measure once you enable the setting. Logs from your own application package are collected by default. To pull in logs from a library or another package, list its prefix with `logsAutoCollectPackageNames` in your `build.gradle.kts`. A prefix matches everything under it, so `androidx.media3` also covers `androidx.media3.exoplayer`, `androidx.media3.common`, and the rest:
```kotlin
measure {
logsAutoCollectPackageNames = listOf("androidx.media3")
}
```
### React Native \[#react-native]
With the setting on, `console` output (`console.debug`, `console.log`, `console.info`, `console.warn`, and `console.error`) is collected on both Android and iOS, with no extra setup.
## Configuration options \[#configuration-options]
Control which logs are collected from the dashboard through [Adaptive Capture](https://measure.sh/docs/adaptive-capture), so you can tune collection without shipping a new build.
| Option | Default | Description |
| ------------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Automatically collect logs** | Off | Collects the logs your app already writes, see [Collect logs automatically](#collect-logs-automatically). Android and React Native only. |
| **Minimum log level** | Warning | The minimum severity to collect. Logs below the selected severity are dropped at the source. |
| **Ignore patterns** | (empty) | Regular expressions matched against the log body. Logs whose body matches any pattern are dropped at the source. |
See [Adaptive Capture](https://measure.sh/docs/adaptive-capture) for the full list.
---
Source: https://measure.sh/docs/logs/ios
---
# iOS
Forward swift-log records to Measure with a custom LogHandler.
On iOS, Measure can't collect logs automatically. Apple's unified logging (`os.Logger`, `os_log`, and `NSLog`) writes to out-of-process system daemons with no public hook to observe, so the SDK can't intercept those calls. If your app uses [swift-log](https://github.com/apple/swift-log), register a `LogHandler` that forwards every record to Measure. Bootstrap it once, before any logging runs:
```swift
import Logging
import Measure
struct MeasureLogHandler: LogHandler {
var metadata = Logger.Metadata()
var logLevel: Logger.Level = .info
subscript(metadataKey key: String) -> Logger.Metadata.Value? {
get { metadata[key] }
set { metadata[key] = newValue }
}
func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?,
source: String, file: String, function: String, line: UInt) {
Measure.log("\(message)", severity: level.measureSeverity)
}
}
private extension Logger.Level {
var measureSeverity: LogSeverity {
switch self {
case .trace, .debug: return .debug
case .info, .notice: return .info
case .warning: return .warning
case .error: return .error
case .critical: return .fatal
}
}
}
// Call once during startup, e.g. in your AppDelegate.
LoggingSystem.bootstrap { _ in MeasureLogHandler() }
```
---
Source: https://measure.sh/docs/logs/flutter
---
# Flutter
Forward logs from the Dart logging package to Measure.
On Flutter, Measure can't collect logs automatically. Dart `print` output and logs from Dart logging frameworks aren't captured. If your app uses the [`logging`](https://pub.dev/packages/logging) package, forward its records to Measure once during startup. Most other logging packages expose a similar hook:
```dart
import 'package:logging/logging.dart';
import 'package:measure_flutter/measure_flutter.dart';
void bootstrapMeasureLogging() {
Logger.root.onRecord.listen((record) {
Measure.instance.log(record.message, severity: record.level.measureSeverity);
});
}
extension on Level {
LogSeverity get measureSeverity {
if (this >= Level.SHOUT) return LogSeverity.fatal;
if (this >= Level.SEVERE) return LogSeverity.error;
if (this >= Level.WARNING) return LogSeverity.warning;
if (this >= Level.INFO) return LogSeverity.info;
return LogSeverity.debug;
}
}
```
---
Source: https://measure.sh/docs/gesture-tracking
---
# Gesture Tracking
Capture taps, long presses, and scrolls automatically along with the UI element each gesture landed on.
Measure captures clicks, long clicks, and scrolls automatically, so you can see how users interact with your app without instrumenting every view. Each gesture records the element it was performed on, and clicks include the text visible on that element, so a tap on a button labeled "Checkout" appears in the session with that text.
Clicks also capture a [layout snapshot](https://measure.sh/docs/gesture-tracking/layout-snapshots), a wireframe of the screen as it looked when the user tapped.
## Android \[#android]
Views and Jetpack Compose are both supported without setup. A gesture on a View is reported with the view's type, its id, and the text on it, so events map directly to your layouts.
Taps on composables that show text are identifiable by that text. To name a composable explicitly, set a [testTag](https://developer.android.com/reference/kotlin/androidx/compose/ui/semantics/package-summary#\(androidx.compose.ui.semantics.SemanticsPropertyReceiver\).testTag\(\)) on it; the tag is reported with the gesture:
```kotlin
Button(
onClick = { viewModel.checkout() },
modifier = Modifier.testTag("checkout_button"),
) {
Text("Checkout")
}
```
## iOS \[#ios]
UIKit gestures are captured without setup. A gesture on a view is reported with the view's type, its accessibility identifier, and the text on it, read from button titles and labels. Scrolls are detected on scrollable views like `UIScrollView`, `UIDatePicker`, and `UIPickerView`.
SwiftUI gestures are captured too, since touches are detected at the window level.
Gestures on SwiftUI screens are reported with SwiftUI's internal view names, and text is not read from SwiftUI elements.
## Flutter \[#flutter]
Gestures are detected once your app is wrapped in `MeasureWidget` during [SDK initialization](https://measure.sh/docs/getting-started/flutter#3-initialize-the-sdk). A custom widget is tracked when it inherits from or contains one of the supported types below; other widgets are ignored.
Clicks and long clicks are detected on these widgets:
| Category | Widgets |
| --------------- | ------------------------------------------------------------------------------------------------------- |
| Buttons | `ButtonStyleButton`, `MaterialButton`, `IconButton`, `FloatingActionButton`, `CupertinoButton` |
| Chips | `InputChip`, `ActionChip`, `FilterChip`, `ChoiceChip` |
| Toggles | `Checkbox`, `Switch`, `Radio`, `CupertinoSwitch`, `CheckboxListTile`, `SwitchListTile`, `RadioListTile` |
| Text fields | `TextField`, `TextFormField`, `CupertinoTextField` |
| Menus and lists | `ListTile`, `PopupMenuButton`, `PopupMenuItem`, `DropdownButton`, `DropdownMenuItem`, `ExpansionTile` |
| Other | `Card`, `GestureDetector`, `Stepper` |
Scrolls are detected on `ListView`, `ScrollView`, `PageView`, and `SingleChildScrollView`.
## React Native \[#react-native]
Gestures are detected by the underlying native SDK, so the Android and iOS behavior above applies as is.
## Performance \[#performance]
### Android \[#android-1]
Finding the gesture target takes 0.458 ms on average for views and 0.658 ms for composables in deep hierarchies ([macro benchmark](https://github.com/measure-sh/measure/pull/377#issue-2123559330)).
### iOS \[#ios-1]
Finding the gesture target takes about 0.2 ms in a typical view hierarchy, and 4 ms in one 1,500 levels deep.
---
Source: https://measure.sh/docs/gesture-tracking/layout-snapshots
---
# Layout snapshots
Capture a lightweight wireframe of the screen with every tap to see the UI a gesture acted on.
Measure captures a layout snapshot with every click: a wireframe of the screen at the moment of the gesture. A snapshot shows what the user was acting on at a fraction of the cost of capturing, storing, and rendering a screenshot.
| Screenshot | Layout snapshot |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|  |  |
Snapshots are throttled to at most one every 750 ms, so bursts of taps don't add overhead.
## Android \[#android]
Snapshots cover the whole screen with no setup, whether it's built with Views, Jetpack Compose, or a mix of both.
## iOS \[#ios]
Snapshots capture the full view hierarchy of the screen with no setup. SwiftUI content appears with its underlying system view names rather than your SwiftUI view names.
## Flutter \[#flutter]
A screen can hold thousands of widgets, so snapshots include only common widget types by default and skip the rest:
| Category | Widgets |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Buttons | `FilledButton`, `OutlinedButton`, `TextButton`, `ElevatedButton`, `CupertinoButton`, `ButtonStyleButton`, `MaterialButton`, `IconButton`, `FloatingActionButton` |
| Menus and lists | `ListTile`, `PopupMenuButton`, `PopupMenuItem`, `DropdownButton`, `DropdownMenuItem`, `ExpansionTile` |
| Structure | `Scaffold`, `CupertinoPageScaffold`, `MaterialApp`, `CupertinoApp`, `Container`, `Row`, `Column`, `Card` |
| Scrolling | `ListView`, `PageView`, `SingleChildScrollView`, `ScrollView` |
| Text | `Text`, `RichText` |
### Include your own widgets \[#include-your-own-widgets]
If your app is built from custom widgets, the default list won't say much about your UI. The [measure\_build](https://github.com/measure-sh/measure/blob/main/flutter/packages/measure_build/README.md) package scans your project and generates a map of every widget type your code declares or uses, including widgets from packages you depend on, so snapshots show your real widget names. Private widgets (names starting with `_`) are left out.
1. **Add the dev dependencies** in `pubspec.yaml`:
```yaml
dev_dependencies:
measure_build: ^0.1.0
build_runner: ^2.4.0
```
2. **Generate the widget map**:
```bash
dart run build_runner build
```
This writes `lib/msr_widgets.g.dart` containing a map named `widgetFilter`. Re-run it after adding new widget types, or use `dart run build_runner watch` to regenerate on every change.
3. **Pass the map to the SDK** during initialization:
```dart
import 'package:measure_flutter/measure_flutter.dart';
import 'package:your_app/msr_widgets.g.dart';
Future main() async {
await Measure.instance.init(
() => runApp(MeasureWidget(child: MyApp())),
config: const MeasureConfig(widgetFilter: widgetFilter),
);
}
```
### Configuration \[#configuration]
The generator works without configuration. To change its defaults, configure it in your app's `build.yaml`:
```yaml
targets:
$default:
builders:
measure_build|widget_analyzer:
options:
output_path: lib/src/msr_widgets.g.dart
scan_directories:
- lib
- custom_widgets
variable_name: msrWidgetFilter
```
* `output_path` sets where the generated file is written. Defaults to `lib/msr_widgets.g.dart`.
* `scan_directories` lists the directories scanned for widgets. Defaults to `lib`; add entries if widget code lives elsewhere.
* `variable_name` names the generated map. Defaults to `widgetFilter`.
## React Native \[#react-native]
Snapshots are generated by the native SDK for the platform the app runs on, so the Android and iOS behavior applies as is.
## Performance \[#performance]
See [Performance Impact](https://measure.sh/docs/performance-impact) for the full benchmark setup.
### Android \[#android-1]
Detecting and creating a snapshot adds 0.6 ms to 1 ms per click.
### iOS \[#ios-1]
Capturing the layout hierarchy takes 7.5 ms at the 95th percentile, measured on an iPhone 14 Plus.
### Flutter \[#flutter-1]
Generating a snapshot and finding the tapped widget takes about 3 ms in a widget tree 50 levels deep, and the time grows linearly with depth ([benchmarks](https://github.com/measure-sh/measure/blob/main/flutter/example/integration_test/layout_snapshot_performance_test.dart)).
---
Source: https://measure.sh/docs/navigation-tracking
---
# Navigation Tracking
Track screen views and lifecycle events as users navigate through your app.
Navigation tracking records the screens users visit, component lifecycle events, and when the app moves to the foreground or background. All of it appears in the session replay.
## Track screen views manually \[#track-screen-views-manually]
Use `trackScreenView` when a screen isn't tracked automatically, like in a custom navigation setup.
```kotlin
import sh.measure.android.Measure
Measure.trackScreenView("Checkout")
```
```swift
import Measure
Measure.trackScreenView("Checkout", attributes: nil)
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.trackScreenViewEvent(name: "Checkout");
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.trackScreenView({ screenName: "Checkout" });
```
```kotlin
import sh.measure.kmp.Measure
Measure.trackScreenView("Checkout")
```
`trackScreenView` also accepts attributes. See [Track screen views](https://measure.sh/docs/api-reference#track-screen-views) in the API reference for examples.
## Automatic collection \[#automatic-collection]
A screen view is recorded each time the user lands on a new screen.
### Android \[#android]
If your app navigates with [AndroidX Navigation](https://developer.android.com/guide/navigation), including Compose, each new destination records a screen view. The [Gradle plugin](https://measure.sh/docs/getting-started/android#3-add-the-gradle-plugin) adds the instrumentation at build time; no code changes are needed.
Works with `androidx.navigation:navigation-compose` versions `2.4.0` to `2.9.8`. Other versions are not instrumented.
### iOS \[#ios]
Screen views aren't recorded automatically. Call [`trackScreenView`](#track-screen-views-manually) from your navigation code.
### Flutter \[#flutter]
Add `MsrNavigatorObserver` to your app's navigator observers to record a screen view on every route change:
```dart
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorObservers: [MsrNavigatorObserver()],
home: HomeScreen(),
);
}
```
### React Native \[#react-native]
Screen views aren't recorded automatically because React Native apps use a variety of navigation libraries. For [React Navigation](https://reactnavigation.org/), hook into the `onStateChange` callback of `NavigationContainer`:
```typescript
import { Measure } from '@measuresh/react-native';
import { NavigationContainer } from '@react-navigation/native';
function App() {
return (
{
const currentRoute = state?.routes[state.index];
if (currentRoute?.name) {
Measure.trackScreenView({ screenName: currentRoute.name });
}
}}
>
);
}
```
## Lifecycle events \[#lifecycle-events]
Lifecycle events are tracked without setup. Flutter and React Native apps get the native Android and iOS events below as well.
### Android \[#android-1]
Activities report created, resumed, paused, and destroyed; fragments report attached, resumed, paused, and detached.
### iOS \[#ios-1]
View controllers report these events:
* `viewDidLoad`
* `viewWillAppear`
* `viewDidAppear`
* `viewWillDisappear`
* `viewDidDisappear`
* `didReceiveMemoryWarning`
To also capture `loadView` and `deinit`, inherit from `MsrViewController` (or `MSRViewController` in Objective-C):
```swift
class CheckoutViewController: MsrViewController {
}
```
For SwiftUI, wrap a view in `MsrMonitorView` or use the `monitorWithMsr` extension to record `onAppear` and `onDisappear`:
```swift
struct CheckoutView: View {
var body: some View {
CheckoutForm()
.monitorWithMsr("Checkout")
}
}
```
---
Source: https://measure.sh/docs/cpu-memory-monitoring
---
# CPU and Memory Monitoring
Capture CPU and memory usage of Android, iOS, Flutter, and React Native apps.
CPU and memory usage are captured every 5 seconds while the app is in the foreground. Readings appear in the session replay.
## CPU usage \[#cpu-usage]
A reading shows how much CPU the app is using. Sustained high usage drains the battery and heats up the device.
### Android \[#android]
Each reading is the average CPU usage since the previous one, across all cores: 100% means every core was fully busy. It's calculated from [`/proc/self/stat`](https://man7.org/linux/man-pages/man5/proc.5.html), where the OS records the clock ticks the process has spent on the CPU:
```
%CPU = 100 × ticks consumed / (clock tick rate × interval × number of cores)
```
### iOS \[#ios]
Each reading is the combined usage of all the app's non-idle threads, where 100% equals one fully busy core. On a device with multiple cores the value can go above 100%. The thread list comes from [`task_threads`](https://web.mit.edu/darwin/src/modules/xnu/osfmk/man/task_info.html) and each thread's usage from [`thread_info`](https://web.mit.edu/darwin/src/modules/xnu/osfmk/man/thread_info.html).
## Memory usage \[#memory-usage]
### Android \[#android-1]
Each reading reports:
* Max heap size: the most memory the runtime lets the app allocate on the Java heap. Allocating past it throws an `OutOfMemoryError`.
* Total heap size and free heap size: the heap memory the runtime has reserved so far, and how much of it is still free.
* RSS (resident set size): the physical memory held by the process, including native code and shared libraries. Shared pages are counted in full, so RSS overstates the app's own usage.
* Total PSS (proportional set size): physical memory with shared pages divided between the processes using them. PSS is the best value for judging the app's overall memory use.
* Native total heap size and native free heap size: total and free memory in the native heap.
The values come from:
| Value | Source |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Heap sizes | [`Runtime.maxMemory`](https://developer.android.com/reference/java/lang/Runtime#maxMemory\(\)), [`Runtime.totalMemory`](https://developer.android.com/reference/java/lang/Runtime#totalMemory\(\)), [`Runtime.freeMemory`](https://developer.android.com/reference/java/lang/Runtime#freeMemory\(\)) |
| Total PSS | [`Debug.getMemoryInfo`](https://developer.android.com/reference/android/os/Debug#getMemoryInfo\(android.os.Debug.MemoryInfo\)) |
| Native heap sizes | [`Debug.getNativeHeapSize`](https://developer.android.com/reference/android/os/Debug#getNativeHeapSize\(\)), [`Debug.getNativeHeapFreeSize`](https://developer.android.com/reference/android/os/Debug#getNativeHeapFreeSize\(\)) |
| RSS | [`/proc/self/statm`](https://man7.org/linux/man-pages/man5/proc.5.html) |
### iOS \[#ios-1]
Each reading reports the app's physical memory footprint: the memory the app is responsible for, including memory the system has compressed. The system uses this value to decide which apps to terminate under memory pressure.
The footprint is read with [`task_info`](https://web.mit.edu/darwin/src/modules/xnu/osfmk/man/task_info.html). When the value is unavailable, the SDK falls back to resident size, which excludes compressed memory and can underreport usage.
[iOS Memory Deep Dive](https://developer.apple.com/videos/play/wwdc2018/416/) explains how Apple defines these values.
## Flutter and React Native \[#flutter-and-react-native]
Flutter and React Native apps don't collect these values themselves. The native Android or iOS SDK collects them using the APIs described above.
---
Source: https://measure.sh/docs/app-launch-metrics
---
# App Launch Metrics
Track cold, warm and hot launch times for Android and iOS apps with full sampling control.
Measure automatically tracks cold, warm and hot app launches along with the time taken for each.
* **Cold Launch**: The time taken to launch the app from scratch.
* **Warm Launch**: The time taken to launch the app from a previously cached state.
* **Hot Launch**: The time taken to launch the app when it is already running in the background.
You can see these metrics in the overview page of the dashboard and set the [sampling values](https://measure.sh/docs/adaptive-capture) as per your needs.

## Android \[#android]
### Cold Launch \[#cold-launch]
A [cold launch](https://developer.android.com/topic/performance/vitals/launch-time#cold) refers to an app starting
from scratch. Cold launch happens in cases such as an app launching for the first time since the device booted or since
the system killed the app.
There are typically two important metrics to track for a cold launch:
1. **Time to Initial Display (TTID)** - the time taken from when the app was launched to when the first frame is displayed.
2. **Time to Full Display (TTFD)** - the time taken from when the app was launched to when the first meaningful content is displayed to the user.
Measuring TTFD is not possible yet; support will be added in a future version.
Meanwhile, \**Time to Initial Display (TTID)*\* is automatically calculated by recording two timestamps:
1. The time when the app was launched.
2. The time when the app's first frame was displayed.
*The time when the app was launched* is calculated differently for different SDK versions. We use the most accurate measurement possible for the given SDK version.
* Up to API 24: the *uptime* when Measure content provider's attachInfo callback is invoked.
* API 24 - API 32: the process start uptime, using [Process.getStartUptimeMillis](https://developer.android.com/reference/android/os/Process#getStartUptimeMillis\(\))
* API 33 and beyond: the process start uptime, using [Process.getStartRequestedUptimeMillis](https://developer.android.com/reference/android/os/Process#getStartRequestedUptimeMillis\(\))
*The time when the app's first frame was displayed* is a bit more complex. Simplifying some of the steps, it is calculated in the following way:
1. Get the decor view by registering [onContentChanged](https://developer.android.com/reference/android/app/Activity#onContentChanged\(\)) callback on the first Activity.
2. Get the next draw callback by registering [OnDrawListener](https://developer.android.com/reference/android/view/ViewTreeObserver.OnDrawListener) on the decor view.
3. [Post a runnable in front of the next draw callback](https://github.com/square/papa/blob/main/papa/src/main/java/papa/internal/Handlers.kt#L8-L13) to record the time just before the first frame was displayed.
### Warm Launch \[#warm-launch]
A [warm launch](https://developer.android.com/topic/performance/vitals/launch-time#warm) refers to the re-launch of an
app causing an Activity `onCreate` to be triggered instead of just `onResume`. This requires the system to recreate
the activity from scratch and hence requires more work than a hot launch.
Warm launch is calculated by keeping track of the time when the Activity `onCreate` of the Activity being recreated is triggered and the time when the first frame is displayed. The same method as for cold launch is used to calculate the time when the first frame is rendered.
### Hot Launch \[#hot-launch]
A [hot launch](https://developer.android.com/topic/performance/vitals/launch-time#hot) refers to the re-launch of an
app causing an Activity `onResume` to be triggered. This typically requires less work than a warm launch as the system
does not need to recreate the activity from scratch. However, if there were any trim memory events leading to the
certain resources being released, the system might need to recreate those resources.
### Further Reading \[#further-reading]
* [Android docs on app startup](https://developer.android.com/topic/performance/vitals/launch-time#warm)
* [Py's android vitals series](https://dev.to/pyricau/series/7827)
* [Py's PAPA GitHub project](https://github.com/square/papa)
## iOS \[#ios]
### Cold Launch \[#cold-launch-1]
A cold launch refers to an app starting up from scratch. Cold launches occur when the app is launched after a reboot or when the app is updated. When an app is launched from scratch, the app is brought from the disk to the memory, iOS loads startup system-side services that support the app, frameworks and daemons that the app depends on to launch might also require re-launching and paging in from disk. Once this is done, the process is spawned.
### Warm Launch \[#warm-launch-1]
Once a cold launch is done, for every subsequent launch, the app still needs to be spanned but the app is still in memory and some of the system-side services are already available. So this launch is a bit faster and a bit more consistent. This type of launch is referred to as the warm launch.
In iOS 15 and later, the system may, depending on device conditions, **pre-warm** your app, launching non-running application processes to reduce the amount of time the user waits before the app is usable. If a app is pre-warmed, we ignore the launch event.
### Hot Launch \[#hot-launch-1]
A hot launch occurs when a user reenters your app from either the home screen or the app switcher. As you know, the app is already launched at this point, so it's going to be very fast. Apple generally refers to this as a `resume` rather than a hot launch.
### Further Reading \[#further-reading-1]
* [WWDC talk on app startup](https://developer.apple.com/videos/play/wwdc2019/423)
* [Reducing your app launch time](https://developer.apple.com/documentation/xcode/reducing-your-app-s-launch-time)
---
Source: https://measure.sh/docs/app-size-monitoring
---
# App Size Monitoring
Track your Android APK or AAB size and iOS IPA size for every build to catch size regressions before they ship.
Measure tracks the size of your app over versions.
## Android \[#android]
The [Gradle plugin](https://measure.sh/docs/getting-started/android#3-add-the-gradle-plugin) uploads app size after every successful `assemble` or `bundle` task. Both APKs and AABs are supported.
APK size is calculated with apkanalyzer's [download-size](https://developer.android.com/tools/apkanalyzer#commands) command, the same tool Android Studio uses. It represents the estimated download size for the end user.
AAB size is calculated with bundletool's [get-size total](https://developer.android.com/tools/bundletool) command. It represents the maximum size of an APK that can be generated from the bundle.
## iOS \[#ios]
IPA size is recorded for each version when you upload dSYMs with either of the [upload scripts](https://measure.sh/docs/error-monitoring/upload-symbols#ios).
The recorded size is the generated `.ipa`, not the App Store download size.
---
Source: https://measure.sh/docs/adaptive-capture
---
# Adaptive Capture
Tune what Measure collects from the dashboard. Adjust sampling, masking, and log collection without releasing a new app version.
Adaptive Capture lets you tune what Measure collects straight from the dashboard, without
releasing a new app version. Adjust sampling rates, screenshot masking, log collection, and
more from the "Apps" tab.
The SDK fetches this configuration and caches it locally, so a change takes two app launches
to take effect. The first launch fetches and caches the new settings, and the next one applies
them.
## Defaults \[#defaults]
Every app starts with these defaults:
| Configuration | Default value |
| ---------------------------------- | ----------------------------------------------------------------------------------- |
| Take screenshot on crash | true |
| Crash session replay duration | 300 seconds (5 minutes) |
| Take screenshot on ANR | true |
| ANR session replay duration | 300 seconds (5 minutes) |
| Bug report session replay duration | 300 |
| Trace sampling rate | 100% |
| Journey sampling rate | 100% |
| Launch metrics sampling rate | 100% |
| Disable HTTP event for URLs | (empty) |
| Track HTTP request body for URLs | (empty) |
| Track HTTP response body for URLs | (empty) |
| Blocked HTTP headers | Authorization, Cookie, Set-Cookie, Proxy-Authorization, WWW-Authenticate, X-Api-Key |
| Screenshot mask level | AllTextAndMedia |
| Automatic log collection | false |
| Minimum log severity | Warning |
| Log ignore patterns | (empty) |
## Errors \[#errors]
These settings apply to crashes and ANRs, the fatal errors.
### Screenshots \[#screenshots]
Each crash and ANR report includes a screenshot of the screen at the moment it happened. Turn
this off if screenshots aren't something you want to capture.
### Session replay duration \[#session-replay-duration]
Every crash and ANR arrives with a replay of the events in the minutes before it. Adjust how
far back that window reaches to trade off detail against data volume. By default it captures the
5 minutes before the error.
## Bug reports \[#bug-reports]
### Session replay duration \[#session-replay-duration-1]
Each bug report carries a replay of what the user did before submitting it. Set how far back
it reaches to capture more or less of the session. By default it goes back 5 minutes.
## Trace sampling \[#trace-sampling]
Measure reports every trace by default. Set the sampling rate anywhere from 100% down to
0.001% to control how much trace data you collect.
## Launch metrics sampling \[#launch-metrics-sampling]
Launch metrics cover cold, warm, and hot launches, shown on the Overview page in the dashboard.
Set a sampling rate to control how many sessions collect them. Every session collects launch
metrics by default, and you can lower the rate to 0.001%.
## Journey sampling \[#journey-sampling]
Journey events power the Journey view in the dashboard. They cover screen view events on all
platforms, activity and fragment lifecycle events on Android, and view controller lifecycle
events on iOS. Set a sampling rate to control how many sessions collect them. Every session
collects journey events by default, and you can lower the rate to 0.001%.
## HTTP events \[#http-events]
HTTP events record the network requests your app makes. Tune what gets collected with the
options below.
### Sampling \[#sampling]
Required minimum SDK versions: Android 0.16.1 and iOS 0.9.2
Set a sampling rate to control how often HTTP events are collected. All of them are collected
by default, and you can lower the rate to 0.001%.
### Collect or ignore specific URLs \[#collect-or-ignore-specific-urls]
Turn HTTP event collection on or off for specific URLs. Match a URL exactly, or use `*` as a
wildcard.
Examples:
* Disable a specific endpoint: `https://example.com/api/v1/users`
* Disable a domain and all its endpoints: `https://example.com/*`
* Disable a specific path across all domains: `*/api/v1/orders`
* Disable a specific URL path: `https://example.com/api/*/payments`
### Request body collection \[#request-body-collection]
Request bodies and headers aren't collected by default. List the URLs you want them collected
for, matching exactly or with `*` wildcards. Enable this only for URLs you trust to be free of
sensitive data and small in payload size.
### Response body collection \[#response-body-collection]
Response bodies and headers aren't collected by default either. List the URLs you want them
collected for, using exact matches or `*` wildcards. As with request bodies, reserve this for
URLs without sensitive data and with small payloads.
### Blocked headers \[#blocked-headers]
Headers are only collected for URLs where you've enabled request or response body collection.
Even then, you can block specific headers by name. Matching is case-insensitive.
These headers are never collected, whether or not you list them:
* Authorization
* Cookie
* Set-Cookie
* Proxy-Authorization
* WWW-Authenticate
* X-Api-Key
## Screenshot mask level \[#screenshot-mask-level]
Mask screenshots collected with crashes and ANRs to keep sensitive information from leaking.
Pick how much to hide.
*The mask level configuration does not apply to SwiftUI screens.*
Because of the way SwiftUI renders its views, all SwiftUI content is masked by default regardless of
the mask level setting. Control masking for individual SwiftUI views with the `.msrMask()` and
`.msrUnmask()` modifiers.
Choose from these levels:
#### Mask all text and media \[#mask-all-text-and-media]
Hides all text, buttons, input fields, images, and video.

#### Mask all text \[#mask-all-text]
Hides all text, buttons, and input fields.

#### Mask text except clickable \[#mask-text-except-clickable]
Hides all text and input fields, but leaves clickable views like buttons visible.

#### Mask sensitive input fields \[#mask-sensitive-input-fields]
Hides sensitive input fields like password, email, and phone fields.

## Logs \[#logs]
The Logs settings control how the SDK collects logs.
### Automatic log collection \[#automatic-log-collection]
*Applies only to Android and React Native.*
Collect logs written to the platform's standard log streams automatically. Logs you track
manually with the `log` API are always collected, whatever this is set to. Off by default.
### Minimum log level \[#minimum-log-level]
Set the lowest severity worth collecting. Anything below it is dropped at the source, for both
automatic logs and logs tracked with the `Measure.log` API. The levels, from lowest to highest,
are `debug`, `info`, `warning`, `error`, and `fatal`. By default Measure keeps `warning` and
above, dropping `debug` and `info`.
### Ignore patterns \[#ignore-patterns]
List regular expressions to match against the log body. Any log whose body matches a pattern is
discarded at the source.
---
Source: https://measure.sh/docs/alerts
---
# Alerts
Get alerted on crash spikes, ANR spikes, and new bug reports, and receive daily summaries of core app metrics over email and Slack.
Measure alerts you over email and Slack when crashes or ANRs spike and when users file bug reports. A daily summary tracks how core metrics change day over day.
## Alert types \[#alert-types]
### Crash and ANR spikes \[#crash-and-anr-spikes]
Crashes and ANRs are [grouped into issues](https://measure.sh/docs/error-monitoring/grouping). Spikes are detected per issue. An alert is sent when a single issue occurs at least 100 times in an hour and those occurrences amount to at least 0.5% of that hour's sessions.
Both thresholds can be changed per app from the Apps page on the dashboard.
Once an issue triggers an alert, the same issue won't trigger another one for 7 days. Other issues can still alert during this period. Handled errors never trigger alerts.
### Bug reports \[#bug-reports]
Every new [bug report](https://measure.sh/docs/bug-reports) triggers an alert with the report's description and a link to it on the dashboard.
### Daily summary \[#daily-summary]
Once a day, you get a summary for your team covering the previous day's core metrics for each of your apps: sessions, crash-free sessions, ANR-free sessions, and cold, warm, and hot launch times. Each metric is compared with the day before, and apps are ordered by session count so the busiest ones come first. Apps that recorded no data that day are left out.
## Channels \[#channels]
Alerts and daily summaries are delivered over email and Slack. Emails go to every member of the team that owns the app. Slack messages go to the channels you've subscribed in your workspace. Set up both in [Integrations](https://measure.sh/docs/integrations).
---
Source: https://measure.sh/docs/agent
---
# Measure Agent
Debug your apps with full context about crashes, errors, sessions and traces from Slack or your coding agent.
Measure Agent allows you to debug faster by answering questions about your app. Ask it to check your app's health, crashes and errors, performance and more!
## What the agent can do \[#what-the-agent-can-do]
Measure Agent has access to all your app's telemetry data (crashes, errors, sessions, traces and metrics) and uses it to help you debug. Ask in natural language and it will count, compare, filter by version or time range, and follow up across a conversation.
## Debug from a coding agent (MCP) \[#debug-from-a-coding-agent-mcp]
Measure Agent is exposed as the `ask_question` tool on Measure's [MCP server](https://measure.sh/docs/mcp). Connect your coding agent (Claude Code, OpenAI Codex, Google Antigravity, Cursor and others) to Measure as described in [Connecting to MCP via Coding Agents](https://measure.sh/docs/mcp#connecting-to-mcp-via-coding-agents), then ask about your telemetry in plain language. Your coding agent calls `ask_question`, gets the answer back, and can use it to help you debug issues, improve performance or run agentic loops.
## Debug from Slack \[#debug-from-slack]
If your workspace is [connected to Slack](https://measure.sh/docs/integrations#slack), you can debug with Measure Agent without leaving Slack:
* In a channel, invite the Measure bot and @mention it with your question, for example "@Measure how many crashes today?". It replies in a thread where you can ask follow-up questions with full conversational context.
* In a direct message, ask the Measure bot directly. Begin with the starter prompts or your own questions and follow up in the same conversation.
Measure matches you to your team account by your Slack profile email, so your Measure email and your Slack email need to match. When a team has more than one app, you can either name the app in your question, or the agent will ask follow-up questions to clarify which app you are querying.
## Example questions \[#example-questions]
* "How is the production app doing on crashes today?"
* "What are the top 5 errors this week?"
* "Show me the slowest network endpoints in version 4.2."
* "How many sessions had slow cold launches in the last 24 hours?"
* "What screen were users on right before the NullPointerException crash?"
On a self-hosted instance, [set up the agent](https://measure.sh/docs/hosting/agent) first.
---
Source: https://measure.sh/docs/mcp
---
# MCP Server
Connect Measure to Claude Code, OpenAI Codex, Google Antigravity, Cursor and other AI coding agents. Query crashes, traces, sessions and bug reports from your editor or AI agent workflows.
Measure exposes a [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets AI-powered coding tools query your app's crash and error data directly.
## What is MCP? \[#what-is-mcp]
The [Model Context Protocol](https://modelcontextprotocol.io) is an open standard that allows AI tools to interact with external data sources through a consistent interface. With Measure's MCP server, you can ask AI assistants to look up crashes, analyze error trends and inspect stack traces without leaving your editor.
## Connecting to MCP via Coding Agents \[#connecting-to-mcp-via-coding-agents]
You can connect your favorite coding agents to Measure as a remote MCP server.
The MCP endpoint is available at:
| Version | Endpoint |
| ----------- | ----------------------------------------- |
| Cloud | `https://agent.measure.sh/mcp` |
| Self Hosted | `https://[your-measure-agent-domain]/mcp` |
Refer to your coding agent's documentation for the specific steps to add a remote MCP server. A few popular agent docs are linked here:
* [**Claude Code**](https://code.claude.com/docs/en/mcp)
* [**OpenAI Codex**](https://developers.openai.com/codex/mcp/)
* [**Google Antigravity**](https://antigravity.google/docs/mcp)
* [**Android Studio**](https://developer.android.com/studio/gemini/add-mcp-server)
* [**VS Code**](https://code.visualstudio.com/docs/agent-customization/mcp-servers)
* [**Cursor**](https://cursor.com/docs/context/mcp)
When you first use a Measure tool, your coding agent will open a browser window for you to sign in to Measure. After authenticating subsequent requests will work automatically.
## Available Tools \[#available-tools]
### `ask_question` \[#ask\_question]
Ask a natural language question to help debug an app and let [Measure Agent](https://measure.sh/docs/agent) work out the answer using the app's telemetry data.
### `list_apps` \[#list\_apps]
List all apps the authenticated user has access to.
### `get_filters` \[#get\_filters]
Get available filter options (versions, OS, countries, devices, etc.) for an app.
### `get_filter_keys` \[#get\_filter\_keys]
List the filter keys of an entity (spans, bug\_reports or builds): the vocabulary a filter expression is written with.
### `get_filter_values` \[#get\_filter\_values]
List the values a filter key can be set to for an app.
### `get_metrics` \[#get\_metrics]
Get app metrics including adoption, crash-free/ANR-free sessions and launch performance (cold/warm/hot p95).
### `get_app_health_over_time` \[#get\_app\_health\_over\_time]
Get the app health timeline: sessions, crashes (fatal exceptions) and ANRs bucketed over time.
### `get_errors` \[#get\_errors]
Get error groups (crashes, non-fatal exceptions and ANRs) for an app, filterable by error type and severity.
### `get_error` \[#get\_error]
Get individual error events (exception or ANR) for a specific error group.
### `get_errors_over_time` \[#get\_errors\_over\_time]
Get time-series of error occurrences across all error groups, filterable by error type and severity.
### `get_error_over_time` \[#get\_error\_over\_time]
Get time-series of occurrences for a specific error group.
### `get_error_distribution` \[#get\_error\_distribution]
Get attribute distribution (OS, device, version, country) for a specific error group.
### `get_error_common_path` \[#get\_error\_common\_path]
Get the most common user navigation path leading to a specific error group.
### `get_sessions` \[#get\_sessions]
Get sessions for an app, ordered by most recent first.
### `get_sessions_over_time` \[#get\_sessions\_over\_time]
Get time-series of session counts.
### `get_session` \[#get\_session]
Get full session with all events.
### `get_bug_reports` \[#get\_bug\_reports]
Get bug reports for an app, ordered by most recent first.
### `get_bug_reports_over_time` \[#get\_bug\_reports\_over\_time]
Get time-series of bug report counts.
### `get_bug_report` \[#get\_bug\_report]
Get a single bug report with full details.
### `update_bug_report_status` \[#update\_bug\_report\_status]
Update the status of a bug report (open or closed).
### `get_root_span_names` \[#get\_root\_span\_names]
Get all root span names for an app.
### `get_span_instances` \[#get\_span\_instances]
Get span instances for a root span name.
### `get_span_metrics_over_time` \[#get\_span\_metrics\_over\_time]
Get p50/p90/p95/p99 duration metrics over time for a span name.
### `get_trace` \[#get\_trace]
Get full trace with all child spans.
### `get_alerts` \[#get\_alerts]
Get alerts for an app, ordered by most recent first.
### `get_journey` \[#get\_journey]
Get an app's journey as a graph of screens. Each link is a transition between consecutive screens within a session, valued by the number of sessions that made that transition.
---
Source: https://measure.sh/docs/api-reference
---
# SDK API Reference
Public API reference for the Measure SDK, with per-platform code examples for Android, iOS, Flutter, React Native and Kotlin Multiplatform.
## Initialize the SDK \[#initialize-the-sdk]
Initialize the SDK with `init` before calling any other method. Call it as early as possible in app startup so crashes, errors and events are captured from the beginning.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.config.MeasureConfig
Measure.init(this, MeasureConfig())
```
```swift
import Measure
let clientInfo = ClientInfo(apiKey: "", apiUrl: "")
Measure.initialize(with: clientInfo, config: BaseMeasureConfig())
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
await Measure.instance.init(
() => runApp(MeasureWidget(child: MyApp())),
config: const MeasureConfig(),
);
```
```typescript
import { Measure, MeasureConfig } from '@measuresh/react-native';
const config = new MeasureConfig({});
await Measure.init({ config });
```
Kotlin Multiplatform has no shared `init`. Initialize each native SDK in its entry point, as shown in the Android and iOS tabs.
### SDK configuration options \[#sdk-configuration-options]
Pass a config object to `init` to customize the SDK. The available options differ by platform.
| Option | Type | Default | Description |
| -------------------------- | ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `enableLogging` | `Boolean` | `false` | Turn on internal SDK logs. |
| `autoStart` | `Boolean` | `true` | Start tracking automatically on init. Set to `false` to delay starting collection. |
| `maxDiskUsageInMb` | `Int` | `50` | Cap the disk space used for buffered data. Clamped between `20MB` and `1500MB`. |
| `trackActivityIntentData` | `Boolean` | `false` | Capture the intent data used to launch an Activity. |
| `requestHeadersProvider` | `MsrRequestHeadersProvider?` | `null` | Add custom HTTP headers to requests the SDK sends to the Measure API, useful for self-hosted setups. |
| `enableFullCollectionMode` | `Boolean` | `false` | Override all sampling and collect every event and trace. Increases cost, so use it for debugging only. |
| `enableDiagnosticMode` | `Boolean` | `false` | Write all SDK logs to a file you can attach when reporting an SDK bug. |
| Option | Type | Default | Description |
| ----------------------------- | ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `enableLogging` | `Bool` | `false` | Turn on internal SDK logs. |
| `autoStart` | `Bool` | `true` | Start tracking automatically on init. Set to `false` to delay starting collection. |
| `maxDiskUsageInMb` | `Int` | `50` | Cap the disk space used for buffered data. Clamped between `20MB` and `1500MB`. |
| `requestHeadersProvider` | `MsrRequestHeadersProvider?` | `nil` | Add custom HTTP headers to requests the SDK sends to the Measure API, useful for self-hosted setups. |
| `enableFullCollectionMode` | `Bool` | `false` | Override all sampling and collect every event and trace. Increases cost, so use it for debugging only. |
| `enableDiagnosticMode` | `Bool` | `false` | Write all SDK logs to a file you can attach when reporting an SDK bug. |
| `enableDiagnosticModeGesture` | `Bool` | `false` | Export SDK logs with a two-finger double-tap share sheet. Requires `enableDiagnosticMode`. |
| Option | Type | Default | Description |
| ---------------------- | ------------------- | ------------ | ----------------------------------------------------------------------------------------------------- |
| `enableLogging` | `bool` | `false` | Turn on internal SDK logs. |
| `autoStart` | `bool` | `true` | Start tracking automatically on init. Set to `false` to delay starting collection. |
| `enableDiagnosticMode` | `bool` | `false` | Write all SDK logs to a file you can attach when reporting an SDK bug. |
| `widgetFilter` | `Map` | built-in set | Widget types to include in layout snapshots. Generate a fuller list with the `measure_build` package. |
| Option | Type | Default | Description |
| ---------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enableLogging` | `boolean` | `false` | Turn on internal SDK logs. |
| `autoStart` | `boolean` | `true` | Start tracking automatically on init. Set to `false` to delay starting collection. |
| `enableDiagnosticMode` | `boolean` | `false` | Write all SDK logs to a file you can attach when reporting an SDK bug. |
| `patchId` | `string?` | none | UUID of the current over-the-air (OTA) patch. Auto-detected with the Metro plugin. Set it manually for OTA systems that bypass Metro, like CodePush. |
| `patchVersion` | `string?` | none | Human-readable label for the OTA patch, like `v1.0.3-hotfix`. |
Kotlin Multiplatform has no shared config. Configure each native SDK directly, as shown in the Android and iOS tabs.
## Start tracking \[#start-tracking]
The SDK starts collecting data automatically after `init`. If you set `autoStart` to `false` in the config, call `start` when you're ready to begin tracking.
```kotlin
import sh.measure.android.Measure
Measure.start()
```
```swift
import Measure
Measure.start()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
await Measure.instance.start();
```
```typescript
import { Measure } from '@measuresh/react-native';
await Measure.start();
```
```kotlin
import sh.measure.kmp.Measure
Measure.start()
```
## Stop tracking \[#stop-tracking]
Pause data collection with `stop`. While stopped, the SDK collects no data. Call `start` to resume.
```kotlin
import sh.measure.android.Measure
Measure.stop()
```
```swift
import Measure
Measure.stop()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
await Measure.instance.stop();
```
```typescript
import { Measure } from '@measuresh/react-native';
await Measure.stop();
```
```kotlin
import sh.measure.kmp.Measure
Measure.stop()
```
## Track errors \[#track-errors]
You can report errors you catch and recover from. These don't crash the app but often point to problems worth fixing. Crashes and ANRs are captured automatically, so you don't need to track those yourself.
### Track a handled error \[#track-a-handled-error]
```kotlin
import sh.measure.android.Measure
try {
methodThatThrows()
} catch (e: Exception) {
Measure.trackHandledException(e)
}
```
```swift
import Measure
// Track a Swift Error or an NSError with trackError
do {
try someThrowingFunction()
} catch {
Measure.trackError(error)
}
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
try {
methodThatThrows();
} catch (e, stackTrace) {
Measure.instance.trackHandledError(e, stackTrace);
}
```
```typescript
import { Measure } from '@measuresh/react-native';
try {
methodThatThrows();
} catch (e) {
Measure.trackError({ error: e });
}
```
```kotlin
import sh.measure.kmp.Measure
try {
methodThatThrows()
} catch (e: Exception) {
Measure.trackHandledException(e)
}
```
### Add attributes \[#add-attributes]
See [Attribute limits](#attribute-limits) for allowed keys and values.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Login").build()
Measure.trackHandledException(e, attributes)
```
```swift
import Measure
Measure.trackError(error, attributes: ["screen": .string("Login")])
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final attributes = AttributeBuilder().add("screen", "Login").build();
Measure.instance.trackHandledError(e, stackTrace, attributes: attributes);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.trackError({ error: e, attributes: { screen: "Login" } });
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Login").build()
Measure.trackHandledException(e, attributes)
```
## Track custom events \[#track-custom-events]
Track app-specific events like user actions or feature usage with `trackEvent`.
* Event names can be up to 64 characters.
* Event names can contain only letters, numbers, hyphens and underscores.
### Track an event \[#track-an-event]
```kotlin
import sh.measure.android.Measure
Measure.trackEvent("event_name")
```
```swift
import Measure
Measure.trackEvent(name: "event_name", attributes: [:])
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.trackEvent(name: "event_name");
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.trackEvent({ name: "event_name" });
```
```kotlin
import sh.measure.kmp.Measure
Measure.trackEvent(name = "event_name")
```
### Set a custom timestamp \[#set-a-custom-timestamp]
Record an event at a specific time, in milliseconds since epoch. Use `getCurrentTime` for an accurate monotonic value.
```kotlin
import sh.measure.android.Measure
Measure.trackEvent("event_name", timestamp = Measure.getCurrentTime())
```
```swift
import Measure
Measure.trackEvent(name: "event_name", attributes: [:], timestamp: Measure.getCurrentTime())
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.trackEvent(name: "event_name", timestamp: Measure.instance.getCurrentTime());
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.trackEvent({ name: "event_name", timestamp: Measure.getCurrentTime() });
```
```kotlin
import sh.measure.kmp.Measure
Measure.trackEvent(name = "event_name", timestamp = Measure.getCurrentTime())
```
### Add attributes \[#add-attributes-1]
See [Attribute limits](#attribute-limits) for allowed keys and values.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("is_premium_user", true).build()
Measure.trackEvent("event_name", attributes = attributes)
```
```swift
import Measure
Measure.trackEvent(name: "event_name", attributes: ["is_premium_user": .boolean(true)])
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final attributes = AttributeBuilder().add("is_premium_user", true).build();
Measure.instance.trackEvent(name: "event_name", attributes: attributes);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.trackEvent({ name: "event_name", attributes: { is_premium_user: true } });
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("is_premium_user", true).build()
Measure.trackEvent(name = "event_name", attributes = attributes)
```
## Track screen views \[#track-screen-views]
The SDK [automatically tracks screen views](https://measure.sh/docs/navigation-tracking) from each platform's navigation system. Record a screen from a custom navigation setup with `trackScreenView`.
### Track a screen view \[#track-a-screen-view]
```kotlin
import sh.measure.android.Measure
Measure.trackScreenView("Home")
```
```swift
import Measure
Measure.trackScreenView("Home", attributes: nil)
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.trackScreenViewEvent(name: "Home");
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.trackScreenView({ screenName: "Home" });
```
```kotlin
import sh.measure.kmp.Measure
Measure.trackScreenView("Home")
```
### Add attributes \[#add-attributes-2]
See [Attribute limits](#attribute-limits) for allowed keys and values.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("source", "deep_link").build()
Measure.trackScreenView("Home", attributes)
```
```swift
import Measure
Measure.trackScreenView("Home", attributes: ["source": .string("deep_link")])
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final attributes = AttributeBuilder().add("source", "deep_link").build();
Measure.instance.trackScreenViewEvent(name: "Home", attributes: attributes);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.trackScreenView({ screenName: "Home", attributes: { source: "deep_link" } });
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("source", "deep_link").build()
Measure.trackScreenView("Home", attributes)
```
### Automatic tracking on Flutter \[#automatic-tracking-on-flutter]
Add `MsrNavigatorObserver` to your app's `navigatorObservers` to track screen views automatically. It works best with named routes.
```dart
import 'package:flutter/material.dart';
import 'package:measure_flutter/measure_flutter.dart';
MaterialApp(
navigatorObservers: [MsrNavigatorObserver()],
home: HomeScreen(),
);
```
## Track performance traces \[#track-performance-traces]
Measure how long any operation takes with a span. A span represents one unit of work. Set a parent to trace a multi-step flow.
* Span names can be up to 64 characters.
* Span names cannot be empty.
### Start a span \[#start-a-span]
Start a span immediately with `startSpan`.
```kotlin
import sh.measure.android.Measure
val span = Measure.startSpan("span-name")
```
```swift
import Measure
let span = Measure.startSpan(name: "span-name")
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("span-name");
```
```typescript
import { Measure } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "span-name" });
```
```kotlin
import sh.measure.kmp.Measure
val span = Measure.startSpan("span-name")
```
### Start with a timestamp \[#start-with-a-timestamp]
Trace an operation that already started by passing a start time from `getCurrentTime`, which returns epoch time from a monotonic clock.
```kotlin
import sh.measure.android.Measure
val span = Measure.startSpan("span-name", timestamp = Measure.getCurrentTime())
```
```swift
import Measure
let span = Measure.startSpan(name: "span-name", timestamp: Measure.getCurrentTime())
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("span-name", timestamp: Measure.instance.getCurrentTime());
```
```typescript
import { Measure } from '@measuresh/react-native';
const span = Measure.startSpanWithTimestamp({ name: "span-name", timestampMs: Measure.getCurrentTime() });
```
```kotlin
import sh.measure.kmp.Measure
val span = Measure.startSpan("span-name", timestamp = Measure.getCurrentTime())
```
### End a span \[#end-a-span]
End a span with `end`. Set the status before ending.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.tracing.SpanStatus
val span = Measure.startSpan("span-name")
span.setStatus(SpanStatus.Ok).end()
```
```swift
import Measure
let span = Measure.startSpan(name: "span-name")
span.setStatus(.ok).end()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("span-name");
span.setStatus(SpanStatus.ok).end();
```
```typescript
import { Measure, SpanStatus } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "span-name" });
span.setStatus(SpanStatus.Ok).end();
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.tracing.SpanStatus
val span = Measure.startSpan("span-name")
span.setStatus(SpanStatus.Ok).end()
```
### End with a timestamp \[#end-with-a-timestamp]
End a span that already finished by passing an end time from `getCurrentTime`.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.tracing.SpanStatus
val span = Measure.startSpan("span-name")
span.setStatus(SpanStatus.Ok).end(timestamp = Measure.getCurrentTime())
```
```swift
import Measure
let span = Measure.startSpan(name: "span-name")
span.setStatus(.ok).end(timestamp: Measure.getCurrentTime())
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("span-name");
span.setStatus(SpanStatus.ok).end(timestamp: Measure.instance.getCurrentTime());
```
```typescript
import { Measure, SpanStatus } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "span-name" });
span.setStatus(SpanStatus.Ok).end(Measure.getCurrentTime());
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.tracing.SpanStatus
val span = Measure.startSpan("span-name")
span.setStatus(SpanStatus.Ok).end(timestamp = Measure.getCurrentTime())
```
### Set the status \[#set-the-status]
Set the outcome of the operation with `setStatus`. Values are `Ok`, `Error` and `Unset` (the default).
```kotlin
import sh.measure.android.Measure
import sh.measure.android.tracing.SpanStatus
val span = Measure.startSpan("span-name")
span.setStatus(SpanStatus.Ok)
```
```swift
import Measure
let span = Measure.startSpan(name: "span-name")
span.setStatus(.ok)
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("span-name");
span.setStatus(SpanStatus.ok);
```
```typescript
import { Measure, SpanStatus } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "span-name" });
span.setStatus(SpanStatus.Ok);
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.tracing.SpanStatus
val span = Measure.startSpan("span-name")
span.setStatus(SpanStatus.Ok)
```
### Set a parent \[#set-a-parent]
Build a hierarchy of operations by setting a parent span with `setParent`.
```kotlin
import sh.measure.android.Measure
val parent = Measure.startSpan("parent-span")
val child = Measure.startSpan("child-span").setParent(parent)
```
```swift
import Measure
let parent = Measure.startSpan(name: "parent-span")
let child = Measure.startSpan(name: "child-span").setParent(parent)
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final parent = Measure.instance.startSpan("parent-span");
final child = Measure.instance.startSpan("child-span").setParent(parent);
```
```typescript
import { Measure } from '@measuresh/react-native';
const parent = Measure.startSpan({ name: "parent-span" });
const child = Measure.startSpan({ name: "child-span" }).setParent(parent);
```
```kotlin
import sh.measure.kmp.Measure
val parent = Measure.startSpan("parent-span")
val child = Measure.startSpan("child-span").setParent(parent)
```
### Add attributes \[#add-attributes-3]
Attach key-value context to a span. Add one attribute at a time, several at once with `setAttributes`, or remove one with `removeAttribute`. See [Attribute limits](#attribute-limits) for allowed keys and values.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.attributes.AttributesBuilder
val span = Measure.startSpan("span-name")
span.setAttribute("key", "value")
span.setAttribute("count", 10)
val attributes = AttributesBuilder().put("key", "value").put("count", 10).build()
span.setAttributes(attributes)
span.removeAttribute("key")
```
```swift
import Measure
let span = Measure.startSpan(name: "span-name")
span.setAttribute("key", value: "value")
span.setAttribute("count", value: 10)
let attributes: [String: AttributeValue] = ["key": .string("value"), "count": .int(10)]
span.setAttributes(attributes)
span.removeAttribute("key")
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("span-name");
span.setAttributeString("key", "value");
span.setAttributeInt("count", 10);
span.setAttributeDouble("ratio", 10.5);
span.setAttributeBool("enabled", true);
final attributes = AttributeBuilder().add("key", "value").add("count", 10).build();
span.setAttributes(attributes);
span.removeAttribute("key");
```
```typescript
import { Measure } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "span-name" });
span.setAttribute("key", "value");
span.setAttribute("count", 10);
span.setAttributes({ key: "value", count: 10, enabled: true });
span.removeAttribute("key");
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.attributes.AttributesBuilder
val span = Measure.startSpan("span-name")
span.setAttribute("key", "value")
span.setAttribute("count", 10)
val attributes = AttributesBuilder().put("key", "value").put("count", 10).build()
span.setAttributes(attributes)
span.removeAttribute("key")
```
### Rename a span \[#rename-a-span]
Update a span's name after it starts with `setName`.
```kotlin
import sh.measure.android.Measure
val span = Measure.startSpan("span-name")
span.setName("updated-name")
```
```swift
import Measure
let span = Measure.startSpan(name: "span-name")
span.setName("updated-name")
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("span-name");
span.setName("updated-name");
```
```typescript
import { Measure } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "span-name" });
span.setName("updated-name");
```
```kotlin
import sh.measure.kmp.Measure
val span = Measure.startSpan("span-name")
span.setName("updated-name")
```
### Add a checkpoint \[#add-a-checkpoint]
Mark a significant moment during a span with `setCheckpoint`. A span can hold up to 100 checkpoints.
```kotlin
import sh.measure.android.Measure
val span = Measure.startSpan("span-name")
span.setCheckpoint("checkpoint-name")
```
```swift
import Measure
let span = Measure.startSpan(name: "span-name")
span.setCheckpoint("checkpoint-name")
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("span-name");
span.setCheckpoint("checkpoint-name");
```
```typescript
import { Measure } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "span-name" });
span.setCheckpoint("checkpoint-name");
```
```kotlin
import sh.measure.kmp.Measure
val span = Measure.startSpan("span-name")
span.setCheckpoint("checkpoint-name")
```
### Defer a span \[#defer-a-span]
Configure a span now and start it later with `createSpanBuilder`.
```kotlin
import sh.measure.android.Measure
val builder = Measure.createSpanBuilder("span-name")
val span = builder?.startSpan()
```
```swift
import Measure
let builder = Measure.createSpanBuilder(name: "span-name")
let span = builder?.startSpan()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final builder = Measure.instance.createSpanBuilder("span-name");
final span = builder?.startSpan();
```
```typescript
import { Measure } from '@measuresh/react-native';
const builder = Measure.createSpanBuilder({ name: "span-name" });
const span = builder?.startSpan();
```
```kotlin
import sh.measure.kmp.Measure
val builder = Measure.createSpanBuilder("span-name")
val span = builder?.startSpan()
```
### Distributed tracing \[#distributed-tracing]
Propagate a trace across services by adding the W3C `traceparent` header to outgoing requests. Get the header key with `getTraceParentHeaderKey` and its value for a span with `getTraceParentHeaderValue`.
```kotlin
import sh.measure.android.Measure
val span = Measure.startSpan("http")
val key = Measure.getTraceParentHeaderKey()
val value = Measure.getTraceParentHeaderValue(span)
```
```swift
import Measure
let span = Measure.startSpan(name: "http")
let key = Measure.getTraceParentHeaderKey()
let value = Measure.getTraceParentHeaderValue(span: span)
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final span = Measure.instance.startSpan("http");
final key = Measure.instance.getTraceParentHeaderKey();
final value = Measure.instance.getTraceParentHeaderValue(span);
```
```typescript
import { Measure } from '@measuresh/react-native';
const span = Measure.startSpan({ name: "http" });
const key = Measure.getTraceParentHeaderKey();
const value = Measure.getTraceParentHeaderValue({ span });
```
```kotlin
import sh.measure.kmp.Measure
val span = Measure.startSpan("http")
val key = Measure.getTraceParentHeaderKey()
val value = Measure.getTraceParentHeaderValue(span)
```
## Track HTTP events \[#track-http-events]
Measure automatically tracks OkHttp on Android and, with the `measure_dio` package, Dio on Flutter. Use `trackHttpEvent` to record requests from any other HTTP client. Use `getCurrentTime` for the start and end time to avoid clock skew.
```kotlin
import sh.measure.android.Measure
val startTime = Measure.getCurrentTime()
// make the request
val endTime = Measure.getCurrentTime()
Measure.trackHttpEvent(
url = "https://api.example.com/users",
method = "GET",
startTime = startTime,
endTime = endTime,
statusCode = 200,
)
```
```swift
import Measure
let startTime = UInt64(Measure.getCurrentTime())
// make the request
let endTime = UInt64(Measure.getCurrentTime())
Measure.trackHttpEvent(
url: "https://api.example.com/users",
method: "GET",
startTime: startTime,
endTime: endTime,
statusCode: 200
)
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final startTime = Measure.instance.getCurrentTime();
// make the request
final endTime = Measure.instance.getCurrentTime();
Measure.instance.trackHttpEvent(
url: "https://api.example.com/users",
method: HttpMethod.get,
startTime: startTime,
endTime: endTime,
statusCode: 200,
);
```
```typescript
import { Measure } from '@measuresh/react-native';
const startTime = Measure.getCurrentTime();
// make the request
const endTime = Measure.getCurrentTime();
Measure.trackHttpEvent({
url: "https://api.example.com/users",
method: "GET",
startTime,
endTime,
statusCode: 200,
});
```
```kotlin
import sh.measure.kmp.Measure
val startTime = Measure.getCurrentTime()
// make the request
val endTime = Measure.getCurrentTime()
Measure.trackHttpEvent(
url = "https://api.example.com/users",
method = "GET",
startTime = startTime,
endTime = endTime,
statusCode = 200,
)
```
## Track bug reports \[#track-bug-reports]
Let users report bugs from inside the app. Open the bug report UI with a single call, or submit a report from your own UI with `trackBugReport`. Descriptions can be up to 4000 characters, with a maximum of 5 attachments. Attach a screenshot with `captureScreenshot`, and trigger reports on device shake.
### Open bug report screen \[#open-bug-report-screen]
```kotlin
import sh.measure.android.Measure
Measure.launchBugReportActivity(takeScreenshot = true)
```
```swift
import Measure
Measure.launchBugReport(takeScreenshot: true)
```
Show the bug report widget, typically pushed as a new screen:
```dart
import 'package:flutter/material.dart';
import 'package:measure_flutter/measure_flutter.dart';
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Measure.instance.createBugReportWidget(),
),
);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.launchBugReport({ takeScreenshot: true });
```
```kotlin
import sh.measure.kmp.Measure
Measure.launchBugReport(takeScreenshot = true)
```
### Track a bug report \[#track-a-bug-report]
Build a custom bug report flow and submit it with `trackBugReport`.
```kotlin
import sh.measure.android.Measure
Measure.trackBugReport(description = "Cart items disappear after reopening the app")
```
```swift
import Measure
Measure.trackBugReport(description: "Cart items disappear after reopening the app")
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.trackBugReport(
description: "Cart items disappear after reopening the app",
attachments: [],
attributes: {},
);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.trackBugReport({ description: "Cart items disappear after reopening the app" });
```
```kotlin
import sh.measure.kmp.Measure
Measure.trackBugReport(description = "Cart items disappear after reopening the app")
```
### Add attributes \[#add-attributes-4]
Attach metadata to a report. Pass attributes when you open the bug report UI or track a report from your own UI. See [Attribute limits](#attribute-limits) for allowed keys and values.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Cart").build()
Measure.launchBugReportActivity(takeScreenshot = true, attributes = attributes)
Measure.trackBugReport(description = "...", attributes = attributes)
```
```swift
import Measure
let attributes: [String: AttributeValue] = ["screen": .string("Cart")]
Measure.launchBugReport(takeScreenshot: true, attributes: attributes)
Measure.trackBugReport(description: "...", attributes: attributes)
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final attributes = AttributeBuilder().add("screen", "Cart").build();
Measure.instance.createBugReportWidget(attributes: attributes);
Measure.instance.trackBugReport(description: "...", attachments: [], attributes: attributes);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.launchBugReport({ takeScreenshot: true, attributes: { screen: "Cart" } });
Measure.trackBugReport({ description: "...", attributes: { screen: "Cart" } });
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Cart").build()
Measure.launchBugReport(takeScreenshot = true, attributes = attributes)
Measure.trackBugReport(description = "...", attributes = attributes)
```
### Shake to report \[#shake-to-report]
Register a shake handler so users can open a bug report by shaking their device. Pass a null handler to disable it.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.bugreport.MsrShakeListener
Measure.setShakeListener(object : MsrShakeListener {
override fun onShake() {
Measure.launchBugReportActivity()
}
})
```
```swift
import Measure
Measure.onShake {
Measure.launchBugReport()
}
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.setShakeListener(() {
// open the bug report widget
});
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.onShake({ handler: () => {
Measure.launchBugReport();
} });
```
Shake to report isn't part of Kotlin Multiplatform's shared API.
## Track logs \[#track-logs]
Record a log at one of five severity levels, using `logDebug`, `logInfo`, `logWarning`, `logError` or `logFatal`. Logs appear in the session replay and add context when debugging. Bodies longer than 1000 characters are truncated.
### Track a log \[#track-a-log]
```kotlin
import sh.measure.android.Measure
Measure.logDebug("Cache miss for key user_42")
Measure.logInfo("User signed in")
Measure.logWarning("Payment failed, retrying")
Measure.logError("Checkout request failed")
Measure.logFatal("Unrecoverable database error")
```
```swift
import Measure
Measure.logDebug("Cache miss for key user_42")
Measure.logInfo("User signed in")
Measure.logWarning("Payment failed, retrying")
Measure.logError("Checkout request failed")
Measure.logFatal("Unrecoverable database error")
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
Measure.instance.logDebug("Cache miss for key user_42");
Measure.instance.logInfo("User signed in");
Measure.instance.logWarning("Payment failed, retrying");
Measure.instance.logError("Checkout request failed");
Measure.instance.logFatal("Unrecoverable database error");
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.logDebug({ body: "Cache miss for key user_42" });
Measure.logInfo({ body: "User signed in" });
Measure.logWarning({ body: "Payment failed, retrying" });
Measure.logError({ body: "Checkout request failed" });
Measure.logFatal({ body: "Unrecoverable database error" });
```
```kotlin
import sh.measure.kmp.Measure
Measure.logDebug("Cache miss for key user_42")
Measure.logInfo("User signed in")
Measure.logWarning("Payment failed, retrying")
Measure.logError("Checkout request failed")
Measure.logFatal("Unrecoverable database error")
```
### Add attributes \[#add-attributes-5]
See [Attribute limits](#attribute-limits) for allowed keys and values.
```kotlin
import sh.measure.android.Measure
import sh.measure.android.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Checkout").build()
Measure.logWarning("Payment failed", attributes)
```
```swift
import Measure
Measure.logWarning("Payment failed", attributes: ["screen": .string("Checkout")])
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final attributes = AttributeBuilder().add("screen", "Checkout").build();
Measure.instance.logWarning("Payment failed", attributes: attributes);
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.logWarning({ body: "Payment failed", attributes: { screen: "Checkout" } });
```
```kotlin
import sh.measure.kmp.Measure
import sh.measure.kmp.attributes.AttributesBuilder
val attributes = AttributesBuilder().put("screen", "Checkout").build()
Measure.logWarning("Payment failed", attributes)
```
## Identify users \[#identify-users]
Set a user ID to correlate sessions with a user when debugging. The ID persists across app launches. Clear it when the user logs out.
Avoid personally identifiable information (PII) like email or phone number in the user ID. Use a hashed or anonymized value instead.
```kotlin
import sh.measure.android.Measure
Measure.setUserId("user-id")
Measure.clearUserId()
```
```swift
import Measure
Measure.setUserId("user-id")
Measure.clearUserId()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
await Measure.instance.setUserId("user-id");
await Measure.instance.clearUserId();
```
```typescript
import { Measure } from '@measuresh/react-native';
Measure.setUserId({ userId: "user-id" });
Measure.clearUserId();
```
```kotlin
import sh.measure.kmp.Measure
Measure.setUserId("user-id")
Measure.clearUserId()
```
## Get session ID \[#get-session-id]
Read the current session ID to correlate app data with a Measure session. Returns `null` if the SDK isn't initialized.
```kotlin
import sh.measure.android.Measure
val sessionId: String? = Measure.getSessionId()
```
```swift
import Measure
let sessionId: String? = Measure.getSessionId()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final String? sessionId = await Measure.instance.getSessionId();
```
```typescript
import { Measure } from '@measuresh/react-native';
const sessionId: string | null = await Measure.getSessionId();
```
```kotlin
import sh.measure.kmp.Measure
val sessionId: String? = Measure.getSessionId()
```
## Get current time \[#get-current-time]
Read epoch time in milliseconds from a monotonic clock. Use it for event, span and HTTP timestamps to avoid clock skew.
```kotlin
import sh.measure.android.Measure
val currentTime: Long = Measure.getCurrentTime()
```
```swift
import Measure
let currentTime: Int64 = Measure.getCurrentTime()
```
```dart
import 'package:measure_flutter/measure_flutter.dart';
final int currentTime = Measure.instance.getCurrentTime();
```
```typescript
import { Measure } from '@measuresh/react-native';
const currentTime: number = Measure.getCurrentTime();
```
```kotlin
import sh.measure.kmp.Measure
val currentTime: Long = Measure.getCurrentTime()
```
## Mask SwiftUI views \[#mask-swiftui-views]
Screenshots mask all SwiftUI content by default, whatever the [mask level](https://measure.sh/docs/adaptive-capture#screenshot-mask-level), because SwiftUI views can't be inspected individually. Reveal a view that isn't sensitive with `.msrUnmask()`, or force masking with `.msrMask()` for a view automatic detection misses, like a standalone `Text` outside a `List`.
```swift
import Measure
VStack {
Text("Order confirmed")
.msrUnmask()
Text(cardNumber)
.msrMask()
}
```
## Mask Flutter widgets \[#mask-flutter-widgets]
Screenshots mask Flutter text, input and image widgets automatically based on the [mask level](https://measure.sh/docs/adaptive-capture#screenshot-mask-level), and a `TextField` with `obscureText` or a password, email or phone keyboard type is always masked. Wrap anything automatic detection misses, like a custom-painted widget showing sensitive data, with `MsrMask` to always redact its area regardless of the mask level.
```dart
import 'package:measure_flutter/measure_flutter.dart';
MsrMask(
child: AccountBalance(amount: balance),
)
```
## Attribute limits \[#attribute-limits]
Attributes are the key-value pairs you attach to events, spans, logs, bug reports, screen views and errors. They follow the same rules everywhere:
* Keys are strings, up to 256 characters.
* Keys can contain only letters, numbers, hyphens and underscores.
* Values are a string, integer, long, double, float or boolean. On React Native, values are a string, number or boolean.
* String values can be up to 256 characters.
---
Source: https://measure.sh/docs/api
---
# Overview
Measure's REST APIs for auth, event ingestion and data fetching, designed around standard REST principles.
Measure exposes two sets of REST APIs. Both accept and return JSON and use standard HTTP response codes.
## SDK endpoints \[#sdk-endpoints]
Used by the Measure SDKs to ingest events, spans and build artifacts from
your apps. You only interact with them directly when building a custom
integration.
## Dashboard API \[#dashboard-api]
Used by the Measure dashboard to read sessions, crashes, ANRs, performance
data and everything else the dashboard shows, and to manage apps, teams and
alerts.
REST APIs contracts are unversioned and subject to change. We will be introducing stable API contracts in the future. For the moment, we do not recommend depending on REST APIs for critical use cases.
---
Source: https://measure.sh/docs/api/sdk/builds/putBuilds
---
# Upload build info and mappings
Measure will use build information like mapping files and build sizes
uploaded via this API for deobfuscation and to track app size changes.
This API only accepts the build metadata and does not actually upload the
files. It returns pre-signed URLs for uploading the files directly to the
returned URLs. For uploading the files, you can issue a standard http request
using cURL or any other way, using the `upload_url` as the URL and `headers`
as the headers for the upload request.
Usage notes:
- Putting `build_size` for the same `version_name`, `version_code` and
`build_type` combination replaces the last size with the latest size.
- Depending on the platform, `build_type` can be `aab`, `apk` for Android or
`ipa` for iOS.
- Depending on the platform, mapping `type` can be `proguard` for Android,
`dsym` or `elf_debug` for iOS, or `jsbundle` for React Native.
- `mappings` is optional. When the `mappings` array is not present, only the
build size information will be updated.
- Each mapping file for iOS must be a gzipped tarball of `dSYM` bundles
ending with a `.tgz` file extension.
- For React Native, upload the JS bundle and its sourcemap as two separate
`jsbundle` mappings in the same request, not bundled together. Each must be
a gzipped tarball ending with `.tgz`, wrapping a single inner file: one
`.tgz` containing the minified JS bundle (for example `main.jsbundle` for
iOS, `index.android.bundle` for Android) and a second `.tgz` containing the
matching sourcemap (for example `main.jsbundle.map`,
`index.android.bundle.map`). Symbolication pairs the two server-side via
the inner filename's `.map` suffix, so the inner filenames must follow the
`` / `.map` convention.
- For Over-The-Air patches (for example CodePush for React Native, Shorebird
for Flutter), use PUT `/builds/ota` instead.
- For mapping filename, only provide the filename, not a path.
- When `mappings` is present, the server returns a mappings array containing
the pre-signed URL for uploading each mapping file.
- Each pre-signed mapping file upload URL has an expiry set which is the same
as the `expires_at` field.
- Each mapping also contains a `headers` object containing all the header
keys and values that should be added in the file upload request. Make sure
all the headers are included in the file upload request, otherwise your
file upload request will fail.
- File upload requests must use the PUT http method. Example:
```sh
curl -X PUT \
--header 'x-amz-meta-mapping_id: 77ac8159-9f0e-4cc7-a9f1-60c05fccd4dc' \
--header 'x-amz-meta-original_file_name: somefile.tgz' \
--data-binary @/path/to/somefile.tgz \
```
---
Source: https://measure.sh/docs/api/sdk/builds/putOtaBuilds
---
# Upload Over-The-Air patch mappings
Uploads mapping files for an Over-The-Air (OTA) patch, a remote update that
ships new code without a new store build (for example CodePush for React
Native, Shorebird for Flutter). It is driven by a `patch_id` (a UUID) and
takes no app version, build number or build size.
Like PUT `/builds`, this API accepts metadata and returns pre-signed URLs
for uploading the mapping files directly. Upload each file with a standard
HTTP PUT using the returned `upload_url` and `headers`.
Usage notes:
- `patch_id` is required and must be a UUID. Tag your OTA patch with a UUID;
it is echoed back on each mapping in the response.
- `patch_version` is optional. Set it to the human-facing version of the
patch (max 256 characters); it is echoed back on each mapping in the
response.
- The app is determined from your API key. Do not send an app identifier in
the body.
- `mappings` is required and must contain at least one mapping. Mapping
`type` can be `proguard`, `dsym`, `elf_debug`, or `jsbundle` (React
Native).
- For React Native, upload the JS bundle and its sourcemap as two separate
`jsbundle` mappings in the same request (same tarball and
`` / `.map` conventions as PUT `/builds`).
- For mapping filename, only provide the filename, not a path.
- Each pre-signed URL expires at `expires_at`, and includes a `headers`
object whose entries must all be added to the upload request, otherwise it
will fail.
- File upload requests must use the PUT http method.
---
Source: https://measure.sh/docs/api/sdk/config/getSdkConfig
---
# Fetch the SDK configuration
Fetches the latest SDK configuration for the app.
Usage notes:
- A successful response returns `200 OK`.
- The response contains an `ETag` header which must be used for caching. The
client must send the `If-None-Match` header with the same `ETag` value in
subsequent requests to this endpoint.
- If the configuration has not changed, the server returns a
`304 Not Modified` response with no body.
- The response contains a `Cache-Control` header which indicates how long
the client can cache the configuration response. Once the cache expires,
the client must revalidate the configuration by sending a request with the
`If-None-Match` header.
---
Source: https://measure.sh/docs/api/sdk/events/putEvents
---
# Ingest a batch of events and spans
Ingests a batch of events, which can be of different types and can range
across multiple sessions.
Usage notes:
- Maximum size of one request must not exceed **9 MB**. The limit includes the combination of events and blob data.
- Each request must contain a unique UUIDv4 id, set as the header `msr-req-id`.
If a request fails, the client must retry the same payload with the same
`msr-req-id` to ensure idempotency.
- Each event must contain a nanosecond precision `timestamp`, for example
`"2023-08-24T14:51:38.000000534Z"`.
- Each request must not contain duplicate event ids.
- Each request must not contain duplicate span ids.
- Each span must contain a nanosecond precision `start_time` and `end_time`.
- Each event must have the following mandatory attributes:
`attribute.installation_id`, `attribute.measure_sdk_version`,
`attribute.thread_name`, `attribute.app_version`, `attribute.app_build`,
`attribute.app_unique_id`.
- Each span must have the following mandatory fields: `name`, `span_id`,
`trace_id`, `session_id`, `status`, `start_time`, `end_time`,
`attributes.installation_id`, `attributes.measure_sdk_version`,
`attributes.app_version`, `attributes.os_version`, `attributes.app_unique_id`.
- At least 1 event must be present in the `events` array field or 1 span must
be present in the `spans` array field. Both arrays must not be empty.
- The request body supports both `application/json` and `multipart/form-data`
requests. Multipart requests will be deprecated in future versions; multipart
only exists for backwards compatibility.
- A successful JSON request returns `200 OK` with attachment upload URLs when
the batch references attachments. A successful multipart request returns
`202 Accepted`.
- Idempotent based on `msr-req-id`. Previously seen requests matching by
`msr-req-id` won't be re-processed; JSON requests receive the attachment
upload info again so the client can proceed to upload attachments if any.
A success response implies the server has accepted the request, but it may
choose to not process and discard some events depending on various conditions.
---
Source: https://measure.sh/docs/api/dashboard/alerts/getAlertsOverview
---
# Fetch an app's alerts
Fetch an app's alerts by applying various optional filters.
Pass `limit` and `offset` values to paginate results.
---
Source: https://measure.sh/docs/api/dashboard/apps/createShortFilters
---
# Create a filter shortcode
Create a shortcode to represent a combination of various app filters.
These shortcodes can then be used in other app APIs which accept
different filters. Shortcodes are short-lived, they'll be automatically
removed after an hour or so.
---
Source: https://measure.sh/docs/api/dashboard/apps/downloadBuildFile
---
# Download a build's mapping file
Download one of a build's mapping files, packaged the way its
platform tooling expects. File ids come from the `files` array of
the builds list response.
The response is the mapping file itself with a `Content-Disposition`
filename, not JSON. Packaging depends on the file's `mapping_type`:
`proguard` returns the mapping file as stored, named `mapping.txt`;
`elf_debug` returns the debug file as stored, named with its uploaded
filename; `jsbundle` returns the bundle or sourcemap file as stored,
named with its uploaded filename; `dsym` returns a zip of the `.dSYM`
bundle reconstructed around the stored DWARF binary, named
`.dSYM.zip`.
---
Source: https://measure.sh/docs/api/dashboard/apps/getAppConfig
---
# Fetch an app's config
Fetch an app's config.
---
Source: https://measure.sh/docs/api/dashboard/apps/getAppFilters
---
# Fetch an app's filters
Fetch an app's filters.
Pass `type=error` to only return filters relevant to exception events
(fatal + nonfatal), `type=anr` for filters relevant to ANR events, or
`type=error,anr` for filters relevant to both exceptions and ANRs. Pass
`ud_attr_keys=1` to return user defined attribute keys. Pass `span=1`
to return filters computed from span data instead of event data. Pass
`builds=1` to return version filters computed from uploaded build
mappings instead of event data. If `type`, `span` and `builds` are
omitted, filters are computed from all events.
`severity` is not honoured on this endpoint and is ignored if passed.
Use it on the errorGroups endpoints instead.
---
Source: https://measure.sh/docs/api/dashboard/apps/getAppJourney
---
# Fetch an app's issue journey map
Fetch an app's issue journey map. Filter time range using `from` & `to`
query string parameters. Filter versions using `versions` &
`version_codes` query string parameters.
---
Source: https://measure.sh/docs/api/dashboard/apps/getAppMetrics
---
# Fetch an app's health metrics
Fetch an app's health metrics. Filter time range using `from` & `to`
query string parameters. Filter version using `versions` &
`version_codes` query string parameters.
Both `versions` & `version_codes` should be present if any one of them
is present, and the number of items in each must be the same. `from` &
`to` values must be ISO 8601 UTC strings in milliseconds precision and
default to a last 7 days time range if not supplied.
Each session and launch metric also carries an `unselected_` variant
holding the same metric computed over the app versions not selected by
the version filters, for comparison against the selected versions.
`no_data` and `unselected_no_data` are true when there was no data to
compute the metric, for example no sessions on that side. The metric
value is then a placeholder zero.
---
Source: https://measure.sh/docs/api/dashboard/apps/getAppRetention
---
# Fetch an app's retention period
Fetch an app's data retention period in days. Must be between 30 and 365. Default is 30.
---
Source: https://measure.sh/docs/api/dashboard/apps/getAppThresholdPrefs
---
# Fetch an app's threshold preferences
Fetch threshold preferences for an app. Returns saved preferences if
present, otherwise returns default values.
Read access is allowed for all team members
(owner/admin/developer/viewer).
---
Source: https://measure.sh/docs/api/dashboard/apps/getBuilds
---
# Fetch an app's builds
Fetch an app's builds, optionally narrowed by upload time and by a
filter expression.
Each build packages the mapping files uploaded for one version of the
app: files are grouped by `version_name`, `version_code` and, for
Over-The-Air patch uploads, `patch_id`, keeping the latest file of each
mapping type. Builds are ordered by their newest file, newest first, and
pagination applies to builds rather than to their files.
A `filter_expr` is applied while files are grouped, so a build whose
files do not all match comes back with only the matching ones.
---
Source: https://measure.sh/docs/api/dashboard/apps/getFilterKeys
---
# Fetch what an entity can be filtered by
Fetches the keys available in an entity, in display order. Each key
includes its label and description, the type of value it accepts, the
operators that type supports, and where its values come from.
`key_groups` lists the groups the keys fall into, also in display order.
Unrelated to `/apps/{id}/filters`, which serves the older fixed set of
app filters.
---
Source: https://measure.sh/docs/api/dashboard/apps/getFilterValues
---
# Fetch the values one filter key can be set to
Fetches the values a single key in an entity can be set to, narrowed
by `search`.
For keys backed by app data, values are read from the entity's own rows,
most recently seen first, with rows where the key is unset excluded. For
keys with a fixed set of values, such as `mapping_type`, values come
from the set the key declares, which a filter cannot go outside.
Only keys with a `value_suggestion_mode` of `full_list` or `sample` can
be listed.
---
Source: https://measure.sh/docs/api/dashboard/apps/getHealthOverviewPlotInstances
---
# Fetch an app's health overview instances plot
Fetch an app's health overview instances plot. The response is a time
series of three metrics bucketed by datetime: sessions, crashes (fatal
exceptions) & ANRs. Filter time range using `from` & `to` query string
parameters. Filter version using `versions` & `version_codes` query
string parameters.
`from` & `to` will default to a last 7 days time range if not supplied.
Both `versions` & `version_codes` should be present if any one of them
is present, and must contain the same number of items. For multiple
comma separated fields, make sure no whitespace characters exist before
or after commas.
The response is an array of three series identified by `id`:
`sessions`, `crashes` & `anrs`. Each series holds a sparse list of
datetime buckets that had data, so a bucket with no crashes or ANRs is
simply absent from that series.
---
Source: https://measure.sh/docs/api/dashboard/apps/renameApp
---
# Rename an app
Modify the name of an app.
---
Source: https://measure.sh/docs/api/dashboard/apps/rotateApiKey
---
# Rotate an app's API key
Rotate an app's API key. All previously active API keys for the app are
revoked. Only the newly generated key stays active.
This endpoint does not require a request body. Existing clients using
previously issued API keys will stop being able to ingest data until
updated with the new key.
---
Source: https://measure.sh/docs/api/dashboard/apps/updateAppConfig
---
# Update an app's config
Update an app's config using a PATCH request.
One or more config fields must be passed in the request body. Only the
fields present in the request body will be updated.
---
Source: https://measure.sh/docs/api/dashboard/apps/updateAppRetention
---
# Update an app's retention period
Update an app's data retention period in days. Must be between 30 and 365.
---
Source: https://measure.sh/docs/api/dashboard/apps/updateAppThresholdPrefs
---
# Update an app's threshold preferences
Update threshold preferences for an app. Only owner/admin roles can
update app threshold prefs.
Validation rules: `error_good_threshold` must be greater than
`error_caution_threshold`; `error_good_threshold` must be in `(0,
100]`; `error_caution_threshold` must be in `[0, 100)`;
`error_spike_min_count_threshold` must be an integer >= 1;
`error_spike_min_rate_threshold` must be in `(0, 100]`.
---
Source: https://measure.sh/docs/api/dashboard/auth/getAuthSession
---
# Fetch session
Fetch session.
Only active sessions that have not been cleaned up will receive a
response. Invalid sessions return an error.
---
Source: https://measure.sh/docs/api/dashboard/auth/refreshToken
---
# Refresh session
Refresh session.
Should pass in refresh token as cookies or in auth header. Set the
session's refresh token in `Authorization: Bearer `
format unless you are using cookies to send refresh tokens.
---
Source: https://measure.sh/docs/api/dashboard/auth/signinGithub
---
# Sign in with GitHub
Sign in with Github.
Should pass in type as "code" along with state and code received from Github.
---
Source: https://measure.sh/docs/api/dashboard/auth/signinGoogle
---
# Sign in with Google
Sign in with Google.
Should pass in type as "code" along with state and code received from Google.
---
Source: https://measure.sh/docs/api/dashboard/auth/signout
---
# Sign out
Sign out.
Only active sessions that have not been cleaned up will receive a
response. Invalid sessions return an error. Set the session's refresh
token in `Authorization: Bearer ` format unless you are
using cookies to send refresh tokens.
---
Source: https://measure.sh/docs/api/dashboard/auth/validateInvite
---
# Validate invite
Validate invite.
Should pass in Invite ID.
---
Source: https://measure.sh/docs/api/dashboard/bug-reports/getBugReport
---
# Fetch a bug report
Fetch a bug report.
---
Source: https://measure.sh/docs/api/dashboard/bug-reports/getBugReportsInstancesPlot
---
# Fetch an app's bug report instances plot
Fetch an app's bug report instances plot, optionally narrowed by a
filter expression.
---
Source: https://measure.sh/docs/api/dashboard/bug-reports/getBugReportsOverview
---
# Fetch an app's bug reports
Fetch an app's bug reports, optionally narrowed by a filter
expression.
Pass `limit` and `offset` values to paginate results.
---
Source: https://measure.sh/docs/api/dashboard/bug-reports/updateBugReportStatus
---
# Update a bug report's status
Update a bug report's status. Status should be 0 (Open) or 1 (Closed).
---
Source: https://measure.sh/docs/api/dashboard/errors/getErrorDetailAttributeDistribution
---
# Fetch an error group's attribute distribution plot
Fetch an error group's attribute distribution, occurrence counts broken
down by app version, OS, country, network type, locale, and device.
Both `versions` & `version_codes` should be present if any one of them
is present.
---
Source: https://measure.sh/docs/api/dashboard/errors/getErrorDetailErrors
---
# Fetch an error group's individual error events
Fetch an error group's individual error events.
Both `versions` & `version_codes` should be present if any one of them
is present.
Each result is either an exception event or an ANR event, distinguished
by the `type` field (`exception` or `anr`). Exception events include
`exception`, `severity`, `num_code`, `code`, `meta`, and
`user_defined_attribute` fields; `severity` is `fatal`, `unhandled`, or
`handled`. ANR events include an `anr` field instead of `exception`;
`severity` is always `fatal` for ANRs and ANR events do not include
`user_defined_attribute`.
---
Source: https://measure.sh/docs/api/dashboard/errors/getErrorDetailPlotInstances
---
# Fetch an app's error detail instances plot
Fetch an app's error detail instances aggregated by date range &
version.
Both `versions` & `version_codes` should be present if any one of them
is present. Both `from` and `to` MUST be present when specifying a date
range.
---
Source: https://measure.sh/docs/api/dashboard/errors/getErrorGroupCommonPath
---
# Fetch an error group's common path
Fetch an error group's common path, the most frequent sequence of
events leading up to the error.
No filter query parameters are accepted. Analyzes up to 50 recent
sessions and returns steps with at least 30% confidence. Returns
`sessions_analyzed: 0` with an empty `steps` array if no path can be
determined.
---
Source: https://measure.sh/docs/api/dashboard/errors/getErrorOverview
---
# Fetch an app's error overview
Fetch an app's error overview. Returns fatal, unhandled & handled
errors plus ANRs unified into a single list.
Both `versions` & `version_codes` should be present if any one of them
is present. When neither `type` nor `severity` is set, all sources
(fatal errors, nonfatal errors, ANRs) are returned.
`custom=true` filters errors to custom-tracked only & does not
suppress ANRs. To exclude ANRs explicitly, use `type=error`.
---
Source: https://measure.sh/docs/api/dashboard/errors/getErrorOverviewPlotInstances
---
# Fetch an app's error overview instances plot
Fetch an app's error overview instances plot aggregated by date range &
version.
Both `versions` & `version_codes` should be present if any one of them
is present. Both `from` and `to` MUST be present when specifying a date
range.
---
Source: https://measure.sh/docs/api/dashboard/network-requests/getNetworkEndpointStatusCodesPlot
---
# Fetch exact HTTP status-code counts over time
Fetch exact HTTP status-code counts over time for a domain, path, or
endpoint selection. At least one of `domain` or `path` is required.
When `domain` is omitted, `path` matches across all domains.
Both `versions` & `version_codes` should be present if any one of them
is present. `from` & `to` will default to a last seven days time range if
not supplied. `status_codes` lists all distinct HTTP status codes
observed. `data_points` contains per-bucket counts keyed by exact
status code (e.g., `count_200`, `count_404`), not by status class.
---
Source: https://measure.sh/docs/api/dashboard/network-requests/getNetworkLatencyPlot
---
# Fetch latency percentiles over time
Fetch latency percentiles for the app, or an optional endpoint selection.
Both `versions` & `version_codes` should be present if any one of them
is present. `from` & `to` will default to a last seven days time range if
not supplied. Latency values (`p50`, `p90`, `p95`, `p99`) are in
milliseconds and may be null.
---
Source: https://measure.sh/docs/api/dashboard/network-requests/getNetworkRequestsEndpoints
---
# List an app's network endpoints
An empty query returns up to 20 generated endpoint patterns. A non-empty
query returns a de-duplicated union of up to 20 matching generated
endpoint patterns and up to 20 matching captured request paths (up to
40 results total). Each source is ranked by request count.
Use `query` to search endpoint text or enter a domain and path pattern.
`from` and `to` default to the last seven days when omitted.
---
Source: https://measure.sh/docs/api/dashboard/network-requests/getNetworkRequestsTrends
---
# Fetch an app's network request trends
Fetch an app's network request trends showing top endpoints by latency,
error rate and frequency.
`from` & `to` will default to a last seven days time range if not
supplied. Results are limited to the top `trends_limit` most
relevant endpoints per category (default 10, max 50). P95 latency is in
milliseconds. Error rate is a percentage (0-100).
---
Source: https://measure.sh/docs/api/dashboard/network-requests/getNetworkStatusCodesPlot
---
# Fetch HTTP status-class counts over time
Fetch HTTP status-class counts over time for the app, or an optional
endpoint selection.
Both `versions` & `version_codes` should be present if any one of them
is present. `from` & `to` will default to a last seven days time range if
not supplied.
---
Source: https://measure.sh/docs/api/dashboard/network-requests/getNetworkTimelinePlot
---
# Fetch an endpoint request timeline
Fetch the request timeline for the app or an optional endpoint selection.
Both `versions` & `version_codes` should be present if any one of them
is present. `from` & `to` will default to a last seven days time range if
not supplied. Results include up to 10 endpoint patterns. `interval` is the time
bucket size in seconds. `elapsed` represents seconds of elapsed time
within sessions. `count` is the average request count per session for
that endpoint in that bucket.
---
Source: https://measure.sh/docs/api/dashboard/prefs/getNotifPrefs
---
# Fetch the current user's notification preferences
Fetch notification preferences for the current user. These preferences
control which types of email notifications the user receives.
---
Source: https://measure.sh/docs/api/dashboard/prefs/updateNotifPrefs
---
# Update the current user's notification preferences
Update notification preferences for the current user. All four boolean
fields must be provided in the request body.
---
Source: https://measure.sh/docs/api/dashboard/sessions/getSession
---
# Fetch an app's session replay
Fetch an app's session replay. The response contains the session's
attributes, CPU and memory usage series, and the session's events
grouped by thread name under `threads`.
---
Source: https://measure.sh/docs/api/dashboard/sessions/getSessionsOverview
---
# Fetch an app's sessions
Fetch an app's sessions by applying various optional filters.
For multiple comma separated fields, make sure no whitespace characters
exist before or after commas. Pass `limit` and `offset` values to
paginate results. When `free_text` is set, each result's
`matched_free_text` field contains a space-separated string of match
labels, e.g. `"CrashType: NullPointerException CrashMessage: something
went wrong"`.
In the response, `meta.next` / `meta.previous` are booleans indicating
whether more pages exist in the respective direction. `events` is
always `null` in list responses; it is populated only on the
single-session detail endpoint. `duration` is the session duration in
milliseconds.
Sessions that contain only a session start event, with no other
activity, are always excluded from results regardless of filters.
---
Source: https://measure.sh/docs/api/dashboard/sessions/getSessionsOverviewPlotInstances
---
# Fetch an app's sessions instances plot
Fetch an app's sessions instances plot by applying various optional
filters.
For multiple comma separated fields, make sure no whitespace characters
exist before or after commas.
Sessions that contain only a session start event, with no other
activity, are always excluded from the instance counts regardless of
filters.
---
Source: https://measure.sh/docs/api/dashboard/teams/changeMemberRole
---
# Change a team member's role
Change role of a member of a team.
---
Source: https://measure.sh/docs/api/dashboard/teams/createApp
---
# Create a new app for a team
Create a new app for a team. The app name of the new app must be passed
in the request body.
The `onboarded` flag in the response indicates whether this app has
received its first session. The `unique_identifier` field is the
package name or bundle id of the app. The `api_key` field is the key
used by the client SDK to send data; its `revoked` field indicates
whether the API key is valid or has been revoked due to security
issues.
---
Source: https://measure.sh/docs/api/dashboard/teams/createTeam
---
# Create a new team
Create a new team. Only owners of existing teams can create new teams.
The access token holder becomes the owner. `name` cannot be empty.
---
Source: https://measure.sh/docs/api/dashboard/teams/deleteTeamSlack
---
# Remove a team's Slack integration
Remove a team's Slack integration. Deletes the stored connection,
including the bot token and subscribed alert channels, along with any
queued Slack alert messages that have not been sent yet. The Measure
app stays installed in the Slack workspace and can be manually removed
from Slack settings.
---
Source: https://measure.sh/docs/api/dashboard/teams/getAuthzRoles
---
# Fetch authorization details for a team
Fetch authorization details of members for a team. Oldest members
appear first in the list of members.
The `can_invite_roles` field indicates what roles new team members can
be invited as by the current user. The `can_change_billing` field
indicates whether the current user is allowed to change billing
settings (upgrade/downgrade plans) for the team; only applicable to the
hosted cloud environment. `can_create_app`, `can_rename_app`,
`can_change_retention`, `can_rotate_api_key`, `can_write_sdk_config`,
`can_rename_team`, `can_manage_slack` and
`can_change_app_threshold_prefs` indicate whether the current user can
perform the corresponding action. The
`current_user_assignable_roles_for_member` field in each member's
`authz` object indicates what roles the current user is allowed to
assign for that particular member, and `current_user_can_remove_member`
indicates whether the current user is allowed to remove that member
from the team.
---
Source: https://measure.sh/docs/api/dashboard/teams/getTeamApp
---
# Fetch details of a team's app
Fetch details of an app for a team.
The `onboarded` flag in the response indicates whether this app has
received its first session. The `unique_identifier` field is the
package name or bundle id of the app. The `api_key` field is the key
used by the client SDK to send data; its `revoked` field indicates
whether the API key is valid or has been revoked due to security
issues.
---
Source: https://measure.sh/docs/api/dashboard/teams/getTeamApps
---
# Fetch a team's apps
Fetch list of apps for a team.
The `onboarded` flag in the response indicates whether this app has
received its first session. The `unique_identifier` field is the
package name or bundle id of the app. The `api_key` field is the key
used by the client SDK to send data; its `revoked` field indicates
whether the API key is valid or has been revoked due to security
issues.
---
Source: https://measure.sh/docs/api/dashboard/teams/getTeamMembers
---
# Fetch a team's members
Fetch list of team members for a team.
---
Source: https://measure.sh/docs/api/dashboard/teams/getTeams
---
# Fetch the access token holder's teams
Fetch list of teams of access token holder.
---
Source: https://measure.sh/docs/api/dashboard/teams/getTeamSlack
---
# Fetch a team's Slack details
Fetch Slack details for a team. Returns the Slack workspace name,
active or inactive status and the comma-separated bot scopes granted at
authorization time if the team has Slack connected. Returns null if the
team doesn't have Slack connected.
---
Source: https://measure.sh/docs/api/dashboard/teams/getUsage
---
# Fetch a team's data usage
Fetch data usage details for a team. Returns data for 3 months
including the current month.
---
Source: https://measure.sh/docs/api/dashboard/teams/getValidTeamInvites
---
# Fetch a team's valid pending invites
Fetch valid pending invites for a team. Only valid invites are
returned. Expired invites are ignored.
---
Source: https://measure.sh/docs/api/dashboard/teams/inviteMembers
---
# Invite new members to a team
Invite new members (both existing & non measure users) to a team.
The email id of the user to be invited, team ID and role of the user to
be invited must be passed in the request body. If an invited user does
not have a measure account, they will get an invite email to sign up
and will be added to the team post signup automatically. If the invited
user already has a measure account, they will be added to the team
immediately.
---
Source: https://measure.sh/docs/api/dashboard/teams/removeInvite
---
# Delete a team invite
Delete a team invite.
---
Source: https://measure.sh/docs/api/dashboard/teams/removeTeamMember
---
# Remove a member from a team
Remove a member from a team.
---
Source: https://measure.sh/docs/api/dashboard/teams/renameTeam
---
# Rename a team
Rename a team. The new name of the team must be passed in the request body.
---
Source: https://measure.sh/docs/api/dashboard/teams/resendInvite
---
# Resend a team invite
Resend a team invite.
---
Source: https://measure.sh/docs/api/dashboard/teams/sendTestSlackAlert
---
# Send test alerts to Slack
Send test alerts to all registered Slack channels.
---
Source: https://measure.sh/docs/api/dashboard/teams/updateTeamSlackStatus
---
# Update a team's Slack integration status
Update active or inactive status for a team's Slack integration.
---
Source: https://measure.sh/docs/api/dashboard/traces/getMetricsPlotForSpanName
---
# Fetch a span's metrics plot
Fetch a span's metrics plot, optionally narrowed by a filter
expression.
---
Source: https://measure.sh/docs/api/dashboard/traces/getRootSpanNames
---
# Fetch an app's root span names
Fetch an app's root span names list with optional filters.
---
Source: https://measure.sh/docs/api/dashboard/traces/getSpansForSpanName
---
# Fetch a span's list of instances
Fetch a span's list of instances, optionally narrowed by a filter
expression.
Pass `limit` and `offset` values to paginate results.
---
Source: https://measure.sh/docs/api/dashboard/traces/getTrace
---
# Fetch a trace
Fetch a trace.
---
Source: https://measure.sh/docs/performance-impact
---
# SDK Performance Impact
Benchmarks for the Measure SDK's impact on app startup and per-event overhead, plus comparison to Firebase initialization.
See the platform-specific sections below for details on the performance impact of the Measure SDK on your app.
## Android \[#android]
### Benchmarks \[#benchmarks]
We benchmark the SDK's performance impact using a Pixel 4a running Android 13 (API 33). Each test runs 35 times using
macro-benchmark. For detailed methodology, see [android/measure-android/benchmarks](https://github.com/measure-sh/measure/blob/main/android/measure-android/benchmarks/README.md).
Benchmark results are specific to the device and the app. It is recommended to run the benchmarks
for your app to get results specific to your app. These numbers are published to provide
a reference point and are used internally to detect any performance regressions.
Benchmarks results for v0.16.0:
* Adds 17.5ms-31.3ms (24.3ms median) to the app startup time (Time to Initial Display) for a simple app.
* Adds 0.6-1ms to detect and create a layout snapshot for click gestures.
### Profiling \[#profiling]
To measure the SDK's impact on your app, we've added traces to key areas of the code. These traces help you track
performance
using [Macro Benchmark](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview)
or by using [Perfetto](https://perfetto.dev/docs/quickstart/android-tracing) directly.
Here's the table:
| Metric | Description |
| --------------------------- | ------------------------------------------------------------------------------------------------------- |
| `msr-init` | Time spent on the **main** thread while initializing |
| `msr-start` | Time spent on the **main** thread when `Measure.start` is called |
| `msr-stop` | Time spent on the **main** thread when `Measure.stop` is called |
| `msr-trackEvent` | Time spent in storing an event to local storage. Almost all of this time is spent *off* the main thread |
| `msr-trackGesture` | Time spent on the **main** thread to track a gesture |
| `msr-generateSvgAttachment` | Time spent on **background** thread to generate an SVG layout |
| `msr-captureScreenshot` | Time spent on **main** thread to capture and compress a screenshot |
| `msr-loadImageFromFile` | Time spent on **main** thread to load an image from a file |
| `msr-loadImageFromUri` | Time spent on **main** thread to load an image from a Uri |
### Comparison to Firebase initialization \[#comparison-to-firebase-initialization]
The following are the results from running a macro-benchmark test to compare initialization
time of Measure SDK vs Firebase. Tested with firebase BOM version `33.7.0` and
Measure Android SDK version `0.10.0` running on a Pixel 4a.
Firebase initializes in multiple phases. The total median time to initialize when running the
benchmark for an app with Firebase crashlytics, performance and analytics SDK
was observed as `77.6ms`. While Measure took `35.0ms` in the same macro-benchmark test.

Perfetto screenshot from one of the runs:

## iOS \[#ios]
## Benchmarks \[#benchmarks-1]
We benchmarked the iOS SDKs performance impact using a baseline app on an iPhone 14 Plus
running iOS 18.5. Each scenario was executed *5 times* and instrumented with `os_signpost` for
precise time tracking. Metrics were collected via Instruments (Time Profiler and Logging with
Signposts).
Performance impact varies based on device and application complexity.
We recommend measuring impact in your specific app.
The following numbers serve as a reference baseline and are used internally to monitor regressions.
### Benchmark Results (v0.6.0) \[#benchmark-results-v060]
Measure adds \**21.03–25.7 ms (avg \~22.8 ms)*\* to app startup time (Time to Initial Display). Other
key operations performed by the SDK can be found below:
| Operation | p95 | Description |
| ------------------------- | ------ | ------------------------------------------------------------ |
| `trackEvent` | 195 µs | Includes event collection, attribute enrichment and queueing |
| `appendAttributes` | 360 µs | Dynamic attribute gathering (e.g., network, device state) |
| `trackBugReport` | 120 µs | Complete flow including screenshot, layout and metadata |
| `trackEventUserTriggered` | 32 µs | User-triggered event tracking |
| `trackSpanTriggered` | 96 µs | When a trace event is emitted |
| `spanProcessorOnStart` | 105 µs | Span construction |
| `spanProcessorOnEnded` | 355 µs | Span serialization and buffering |
| `generateScreenshot` | 80 ms | Snapshotting and compression of UI |
| `generateLayoutSnapshot` | 7.5 ms | Layout hierarchy capture |
---
Source: https://measure.sh/docs/integrations
---
# Integrations
Connect email and Slack to receive alerts and daily summaries. Choose the Slack channels that receive them and debug with Measure Agent without leaving Slack.
Integrations deliver your [alerts](https://measure.sh/docs/alerts) and daily summaries. Email reaches every team member with no setup. Connecting Slack sends alerts to the channels you choose and lets you ask [Measure Agent](https://measure.sh/docs/agent) about your app's crashes, errors, and performance without leaving Slack.
## Email \[#email]
All members of a team receive alert emails and daily summaries for the team's apps. Each member can turn individual alert types on or off for themselves from the **Notifications** page on the dashboard. Everything is on by default.
## Slack \[#slack]
Connect Slack to get alerts and daily summaries in the channels you choose, and to debug with Measure Agent without leaving Slack.
### Connect your workspace \[#connect-your-workspace]
Navigate to the **Team** settings page on the dashboard and click the **Add to Slack** button. This starts an OAuth flow to authorize the Measure Slack app for your workspace.
Once connected, click **Send Test Alert** to verify the connection is working. It sends a test message to every subscribed channel.
### Set up alerts \[#set-up-alerts]
Manage which channels receive alerts and daily summaries with the Measure bot:
* **Subscribe a channel**: invite the Measure bot to the channel, then run `/subscribe-alerts` in it. Subscribe as many channels as you like.
* **Unsubscribe a channel**: run `/stop-alerts` in the channel.
* **List subscribed channels**: run `/list-alert-channels` in any channel the bot has been added to.
To stop all alerts at once, turn off the Slack integration toggle on the **Team** settings page. Channel subscriptions are preserved and resume if you re-enable it.
### Debug with Measure Agent \[#debug-with-measure-agent]
Once connected, ask [Measure Agent](https://measure.sh/docs/agent) questions about your app directly in Slack. Invite the Measure bot to a channel and @mention it with a question, like "@Measure how many crashes today?", or message it directly. It answers using your app's telemetry (crashes, errors, sessions, and traces) and keeps the conversation's context across follow-up questions.
See [Debug from Slack](https://measure.sh/docs/agent#debug-from-slack) for details, including how your Slack account is matched to your Measure account.
## Self-hosted setup \[#self-hosted-setup]
On a self-hosted instance, both integrations need one-time configuration:
* [Set up SMTP email](https://measure.sh/docs/hosting/smtp-email) to deliver alert emails and daily summaries.
* [Set up the Slack app](https://measure.sh/docs/hosting/slack) before connecting your workspace.
---
Source: https://measure.sh/docs/hosting
---
# Self Hosting
Self-host Measure to monitor crashes, ANRs and performance for your mobile apps. Deploy on a Linux VM and configure OAuth, SMTP and Slack.
This guide helps you to self-host measure.sh on your own infrastructure.
Self-hosting at scale requires knowledge of:
* Security
* Networking
* Containers and orchestration
* Server administration
* Database management, backups and scaling
* Distributed systems
Incorrect configurations can lead to:
* Data loss
* Security vulnerabilities
* Downtime
It is most useful for heavily regulated environments where cloud hosting is not an option and experienced infra engineers are available for deployment, monitoring and ongoing management.
Our self host install script is designed for single machine setups. For distributed, secure and scalable hosting, we recommend our [hosted cloud](https://measure.sh).
## Objectives \[#objectives]
* Self host measure on a single VM instance
* Install and configure `caddy` as a reverse proxy
* Create and configure a Google OAuth application
* Create and configure a GitHub OAuth application
## Prerequisites \[#prerequisites]
* Basic terminal/command line skills
* Basic text editor skills
* SSH access to a Cloud VM instance
* Ability to add DNS A records on your primary domain
* Ability to run commands with `sudo`
* `git` in your PATH
* External IP of the VM
## System Requirements \[#system-requirements]
* x86-64/amd64 Linux Virtual Machine
* Any one of the following supported Linux distributions
* Ubuntu 24.04 LTS
* Debian 12 (Bookworm)
* At least 4 vCPUs
* At least 16 GB RAM
* At least 100 GB of boot disk volume
* Port `80` and `443` opened in firewall settings
## Deploy on a Linux virtual machine \[#deploy-on-a-linux-virtual-machine]
Follow these step-by-step instructions to deploy measure.sh on a single Linux VM instance.
### 1. SSH into your VM \[#1-ssh-into-your-vm]
Deploy a Linux VM meeting the above system requirements on any popular Cloud hosting provider like Google Cloud Platform, AWS or DigitalOcean. Once the machine is up and running, SSH into it following your cloud provider's instructions.
### 2. Clone the measure repo \[#2-clone-the-measure-repo]
Let's start by moving to your home directory.
```sh
cd ~
```
Choose a git tag. You can find out the latest stable release tag from the [releases](https://github.com/measure-sh/measure/releases) page.
Always choose a tag matching the format `v[MAJOR].[MINOR].[PATCH]`, for example: `v1.2.3`.
These tags are tailored for self host deployments.
Clone the repository with git and change to the `measure` directory. Replace `GIT-TAG` with your chosen git tag.
```sh
git clone https://github.com/measure-sh/measure.git -b GIT-TAG && cd measure
```
### 3. Run the `install.sh` script \[#3-run-the-installsh-script]
Next, change into the `self-host` directory. All successive commands will be run from this directory.
```sh
cd self-host
```
Run the install script with `sudo`.
```sh
sudo ./install.sh
```
To use **podman** instead of \**docker*\*, use the *--podman* flag.
```sh
sudo ./install.sh --podman
```
This would install the following packages.
* [podman](https://podman.io/)
* [podman-docker](https://packages.debian.org/bookworm/podman-docker)
* [podman-compose](https://github.com/containers/podman-compose)
* [docker-compose](https://github.com/docker/compose)
You can continue to use regular docker commands like, `docker ps -a` or `docker compose ps -a`. It should work seamlessly.
The measure.sh install script will check your system's requirements and start the installation. It can take a few minutes to complete.
### 4. Configure and start your self hosted measure instance \[#4-configure-and-start-your-self-hosted-measure-instance]
During installation, you'll be presented with the Measure configuration wizard.
For the first prompt, it'll ask for a namespace for your company or team. This typically will be your company or team's name. If trying out individually, feel free to set any name.
For the next prompt, you'll be asked to enter the URL to access measure.sh's web dashboard. Typically, this might look like a subdomain on your primary domain, for example, if your domain is `yourcompany.com`, enter `https://measure.yourcompany.com`.
Next, you'll be asked to enter the URL to access Measure's REST API & Ingest endpoint. Typically, this might look like, `https://measure-api.yourcompany.com` & `https://measure-ingest.yourcompany.com` respectively.
Later in this guide, you'll be setting DNS A records for the above subdomains you entered. For now, let's move on to the next prompt.
For the next few prompts, you'll need to obtain a Google & GitHub OAuth Application's credentials. This is required to setup authentication in measure.sh dashboard. Follow the below links to obtain Google & GitHub OAuth credentials.
* [Create a Google OAuth App](https://measure.sh/docs/hosting/google-oauth)
* [Create a GitHub OAuth App](https://measure.sh/docs/hosting/github-oauth)
Once you have created the above apps, copy the key and secrets and enter in the relevant prompts.
Next, you'll need to set up an SMTP email provider. This is used to send emails for team invites, alerts & so on. Follow the below link to obtain SMTP credentials:
* [Set up SMTP email provider](https://measure.sh/docs/hosting/smtp-email)
Once your provider is set up, copy the values and enter in the relevant prompts.
Optionally, you can set up a Slack app if you want to receive alert notifications in your Slack workspace. Follow the below link to create and configure a Slack app:
* [Set up Slack Integration](https://measure.sh/docs/hosting/slack)
Once your slack integration is set up, copy the values and enter in the relevant prompts. If you wish to ignore it, enter empty values and proceed.
Optionally, you can set up Measure Agent so you can debug your app from your coding agent or Slack. Follow the below link to configure it:
* [Set up Measure Agent](https://measure.sh/docs/hosting/agent)
Once set up, copy the API key and model values and enter them in the relevant prompts. If you wish to ignore it, enter empty values and proceed.
Once completed, the install script will attempt to start all the Measure docker compose services.
At this point, all the services should be up, but they are not reachable from the internet. To make sure these services can serve traffic, let's setup:
* A reverse proxy using [caddy](https://caddyserver.com/)
* Setup DNS A records on your domain
### 5. Setup a reverse proxy server \[#5-setup-a-reverse-proxy-server]
While we recommend [caddy](https://caddyserver.com) for routing incoming requests to the correct destinations. You can setup any other reverse proxy server of your choice, like [nginx](https://nginx.org/) or [traefik](https://traefik.io/). We chose Caddy because it's relatively straightforward to setup and comes with great defaults.
For now, let's setup caddy.
Change to your home directory.
```sh
cd ~
```
Run the following commands to install caddy.
```sh
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl && \
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg && \
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list && \
sudo apt update && \
sudo apt install caddy
```
If you are not installing on Ubuntu or Debian, please follow the guide on Caddy's [installation page](https://caddyserver.com/docs/install) and come back here when caddy is installed.
Create a basic `~/Caddyfile` config by running the following.
```sh
cat < ~/Caddyfile
measure.yourcompany.com {
reverse_proxy http://localhost:3000
}
measure-api.yourcompany.com {
reverse_proxy http://localhost:8080
}
measure-ingest.yourcompany.com {
reverse_proxy http://localhost:8085
}
EOF
```
In the above Caddyfile, we have used the example domains from above, but make sure you replace with your actual domain names.
Next, reload caddy to make sure caddy picks up our newly generated config.
```sh
caddy reload
```
### 6. Setup DNS A records \[#6-setup-dns-a-records]
For this last step, we'll setup 2 DNS A records and put those subdomains to work. First, obtain your VM's external IP address. Let's say, the external IP is `101.102.103.104`.
Go to your domain hosting provider and add A records for the following subdomains.
```
measure.yourcompany.com IN A 101.102.103.104
measure-api.yourcompany.com IN A 101.102.103.104
measure-ingest.yourcompany.com IN A 101.102.103.104
```
Depending on your domain provider, it might take a few minutes to couple of hours for the above DNS records to take effect.
### 7. Access your measure.sh dashboard \[#7-access-your-measuresh-dashboard]
Visit `https://measure.yourcompany.com` to access your dashboard and sign in to continue. Replace `yourcompany.com` with your domain.
## Upgrade a Self Hosted Installation \[#upgrade-a-self-hosted-installation]
To upgrade to a specific or latest version of measure.sh, SSH to your VM instance first and run these commands.
For certain target versions, you will need to run extra migration scripts. Check out our [Migration Guides](https://measure.sh/docs/hosting/migration-guides).
```sh
# change to the directory you
# had cloned to.
cd ~/measure
```
Find out the suitable version from the [list of release tags](https://github.com/measure-sh/measure/releases). \**We recommend sticking to the latest stable release.*\*
Always choose a tag matching the format `v[MAJOR].[MINOR].[PATCH]`, for example: `v1.2.3`.
These tags are tailored for self host deployments.
Run `git fetch --tags` to fetch all tags.
```sh
git reset --hard # reset local modifications, if any
git fetch --tags
```
To see which tag you are on, run: `git describe --tags --always` from `self-host` directory.
Checkout to a particular git tag.
```sh
# replace `v1.2.3` with the suitable git tag
git checkout v1.2.3
```
Change to `self-host` directory and run `sudo ./install.sh` to perform the upgrade.
```sh
# change to `self-host` directory
cd self-host
# run the `install.sh` script
sudo ./install.sh
```
It'll take a few minutes for the upgrade to complete.
Please note that an upgrade may not happen smoothly because of incompatible changes or configuration mismatches. If you face any issues while upgrading or need advice, please do not hesitate to [open an issue](https://github.com/measure-sh/measure/issues/new/choose) or to drop a message on our [Discord](https://discord.gg/f6zGkBCt42).
## Run on macOS locally \[#run-on-macos-locally]
You can run measure.sh locally on macOS for trying it out quickly, but keep in mind that not all features may work as expected on macOS.
### macOS Compatibility \[#macos-compatibility]
Not all features on macOS may work as expected. Don't use this setup for production. This guide was tested on macOS 14.6, though older or newer versions of macOS may work too.
### Using Podman on macOS \[#using-podman-on-macos]
Podman on macOS runs containers inside a virtual machine. Make sure to allocate sufficient memory (at least 8 GB)
to the podman machine. Low memory may crash the application or lead to instability.
### System Requirements \[#system-requirements-1]
Make sure the following requirements are met before proceeding.
| Name | Version |
| -------------- | -------- |
| Docker | v26.1+ |
| Podman | v5.0.3+ |
| Docker Compose | v2.27.3+ |
| node | v20+ |
### 1. Clone the measure repo \[#1-clone-the-measure-repo]
Choose a git a tag to use. You can find out the latest stable release tag from the [releases](https://github.com/measure-sh/measure/releases) page.
Always choose a tag matching the format `v[MAJOR].[MINOR].[PATCH]`, for example: `v1.2.3`.
These tags are tailored for self host deployments.
Clone the repository with git and change to the `measure` directory. Replace `GIT-TAG` with your chosen git tag.
```sh
git clone https://github.com/measure-sh/measure.git -b GIT-TAG && cd measure/self-host
```
### 2. Run `config.sh` script to configure \[#2-run-configsh-script-to-configure]
Run the `config.sh` script to auto configure most settings.
```sh
./config.sh
```
For production usage, use the *--production* flag.
```sh
./config.sh --production
```
To continue, you'll need to obtain a Google & GitHub OAuth Application's credentials. This is required to setup authentication in Measure dashboard. Follow the below links to obtain Google & GitHub OAuth credentials.
* [Create a Google OAuth App](https://measure.sh/docs/hosting/google-oauth)
* [Create a GitHub OAuth App](https://measure.sh/docs/hosting/github-oauth)
Once you have created the above apps, copy the key and secrets and enter them in the relevant prompts.
Next, you'll need to set up an SMTP email provider. This is used to send emails for team invites, alerts & so on. Follow the below link to obtain SMTP credentials:
* [Set up SMTP email provider](https://measure.sh/docs/hosting/smtp-email)
Once your provider is set up, copy the values and enter them in the relevant prompts.
### 3. Start the containers \[#3-start-the-containers]
To start the containers in production mode, run.
```sh
docker compose -f compose.yml -f compose.prod.yml \
--profile migrate \
up --build
```
It'll take a few seconds for the containers to be healthy.
### 4. Access your Measure dashboard \[#4-access-your-measure-dashboard]
Visit [Dashboard](http://localhost:3000/auth/login) to access your dashboard and sign in to continue.
## Frequently Asked Questions \[#frequently-asked-questions]
Typical questions asked by other self host-ers.
### Q. Can I use podman instead of docker? \[#q-can-i-use-podman-instead-of-docker]
Yes, you can. Use the `--podman` flag when running the installation script.
```sh
sudo ./install.sh --podman
```
You can administer the instance using docker and docker compose commands as if you were using docker.
### Q. I made some mistake and want to start the installation over? \[#q-i-made-some-mistake-and-want-to-start-the-installation-over]
If you want to start over the installation from a clean slate, do the following.
1. **Run the following from the `self-host` directory**
```sh
sudo docker compose down --rmi all --remove-orphans --volumes
```
2. **Remove the cloned `measure` directory**
```sh
rm -rf ~/measure
```
3. **Repeat the installation process from start**
### Q. How to perform healthcheck of Measure services? \[#q-how-to-perform-healthcheck-of-measure-services]
To perform health check for the API service, use:
```sh
curl -s https://measure.yourcompany.com | grep measure
# local environment
curl -s http://localhost:3000 | grep measure
```
To perform health check for the Dashboard service, use:
```sh
curl -s https://measure-api.yourcompany.com/ping | grep pong
# local environment
curl -s http://localhost:8080/ping | grep pong
```
To perform health check for the Ingest service, use:
```sh
curl -s https://measure-ingest.yourcompany.com/ping | grep pong
# local environment
curl -s http://localhost:8085/ping | grep pong
```
Replace the domain names accordingly. These health check endpoints are useful when defining Measure services as backends for a load balancer or proxy.
### Q. Can I host Measure behind a VPN? \[#q-can-i-host-measure-behind-a-vpn]
Absolutely! Hosting Measure behind a VPN is a great way to shield it from public internet. Though, keep the following in mind.
1. **Measure API service must be accessible on public internet.** This allows the Measure SDK in your mobile app to communicate to the Measure backend.
2. **Measure Dashboard service must bind on the private address.** Typically, proxy servers will listen on all network interfaces. When hosting behind a VPN, make sure to bind the Dashboard service on a private IP only. This is essential to achieve network level isolation. For example, the Caddy configuration would look like:
```
measure.yourcompany.com {
# listen only on private IP
# change the IP accordingly
bind 10.0.0.1
reverse_proxy http://localhost:3000
}
measure-api.yourcompany.com {
reverse_proxy http://localhost:8080
}
measure-ingest.yourcompany.com {
reverse_proxy http://localhost:8085
}
```
[Read more on `bind`.](https://caddyserver.com/docs/caddyfile/directives/bind)
In the above setup, only authorized VPN users will be able to access the Measure Dashboard, without disrupting ingestion of events coming from Measure SDKs.
### Q. I'm using nginx as a reverse proxy. What configurations should I change? \[#q-im-using-nginx-as-a-reverse-proxy-what-configurations-should-i-change]
When using nginx, configure the following directives.
* **`client_max_body_size`**. Set it to sufficiently large like `1024M` (1 GiB) to ensure large debug mapping files, like proguard & dSYM file uploads will succeed.
* **`ignore_invalid_headers`**. Set this to `off`, otherwise uploading builds or mapping files like proguard & dSYM files may fail.
```
server {
# other configuration
client_max_body_size 1024M;
ignore_invalid_headers off;
# other configuration
}
```
### Q. How to add or update environment variables? \[#q-how-to-add-or-update-environment-variables]
All configuration variables are defined in the `self-host/.env` file. For the updated configuration to take effect, shutdown & start compose services.
To do that, run from inside the `self-host` directory.
```sh
sudo docker compose -f compose.yml -f compose.prod.yml \
--profile migrate \
down
```
Then run the `./install.sh` script.
```sh
sudo ./install.sh
```
### Q. How to setup complete symbolication for iOS? \[#q-how-to-setup-complete-symbolication-for-ios]
To symbolicate iOS frames for system frameworks, you would need to obtain a Google Drive API key & do the following:
1. Update & save the `DRIVE_API_KEY` environment variable in `self-host/.env`
2. Restart the `symboloader` service by running
```sh
docker compose down symboloader
docker compose up -d symboloader
```
3. Run symboloader's sync command, like this
```sh
docker compose exec symboloader symboloader \
sync \
--versions "last 5 versions"
```
Few things to note:
* iOS system symbol files can occupy a lot of disk space. Make sure you have at least 500 GB additional disk space capacity.
* You may be rate-limited by Google Drive if you receive a 403 error: *We're sorry... but your computer or network may be sending automated queries*. When this happens, retry after 24 hours.
[Read about the symboloader CLI commands](https://github.com/measure-sh/measure/blob/main/backend/symboloader/README.md)
### Q. Why does ClickHouse consume high amount of CPU or memory? \[#q-why-does-clickhouse-consume-high-amount-of-cpu-or-memory]
ClickHouse is engineered to maximize hardware utilization, often leading to high CPU and memory consumption. In an idle state, when Measure is not ingesting sessions or executing queries, you might observe 25-30% CPU consumption. Under higher load, CPU consumption may go up to 90-100%. This is completely normal and expected behavior.
Several factors contribute to this behavior.
1. **Query Execution and Parallelism**: ClickHouse executes queries using multiple threads to enhance performance. By default, it utilizes a number of threads equal to the number of available CPU cores.
2. **Background Merges and Mutations**: ClickHouse continuously merges data parts in the background to optimize storage and query performance. These merge operations and data mutations can lead to increased resource consumption.
3. **Compression and Decompression**: ClickHouse employs compression algorithms to minimize storage space. Compressing and decompressing data during ingestion and queries are CPU-intensive operations.
4. **Hardware Considerations**: ClickHouse is configured to utilize available resources effectively and expects adequate RAM (32 GB or more is recommended). Our default configuration is designed to strike a balance between cost and performance for majority of users. Feel free to allocate additional system resources if your budget allows.
Having said that, we'll continue to optimize our configuration and recommendation over time to accommodate light & heavy weight usage patterns whenever possible.
If you want to discuss more, hop on to our [Discord](https://discord.gg/f6zGkBCt42) and ask your questions.
#### References \[#references]
1. [ClickHouse High CPU Usage](https://kb.altinity.com/altinity-kb-setup-and-maintenance/high-cpu-usage/)
2. [GitHub issue discussing mutations](https://github.com/ClickHouse/ClickHouse/issues/39403)
3. [ClickHouse Usage Recommendations](https://clickhouse.com/docs/en/operations/tips)
---
Source: https://measure.sh/docs/hosting/google-oauth
---
# Set up Google OAuth
Set up a Google OAuth application for your self-hosted Measure instance. Configure the consent screen, scopes and redirect URIs.
In this guide, we'll help you setup a Google OAuth app so that your users can login using their Google accounts on your Measure Web dashboard.
1. Visit [console.cloud.google.com](https://console.cloud.google.com)
2. Open the hamburger menu on top left and hover above **APIs & Services** and click on **OAuth consent screen** from the fly-out menu
3. On the next screen, choose User Type as **Internal**
4. Enter an appropriate app name and user support email
5. Add a logo of your company or team
6. Add the top-level domain of your company
7. Add a developer contact email
8. In the scopes screen, choose the following scopes and click **UPDATE**
1. `../auth/userinfo.email`
2. `../auth/userinfo.profile`
9. Click on **SAVE AND CONTINUE**
10. On the next screen, review all info and click on **BACK TO DASHBOARD** when done
11. On the left sidebar, click on **Credentials**
12. Click on the **+ CREATE CREDENTIALS** button and choose **OAuth client ID**
13. Select **Web application**
14. Enter a name for the application
15. Under **Authorized JavaScript origins**, enter your Measure dashboard URL (Example: [https://measure.yourcompany.com](https://measure.yourcompany.com)). Replace `yourcompany.com` with your domain.
16. Under **Authorized redirect URIs**, enter the redirect URI in the following way: [https://measure.yourcompany.com/auth/callback/google](https://measure.yourcompany.com/auth/callback/google). Replace `yourcompany.com` with your domain.
17. Click **CREATE**
18. Copy the **Client ID** and the **Client Secret**
---
Source: https://measure.sh/docs/hosting/github-oauth
---
# Set up GitHub OAuth
Set up a GitHub OAuth application for your self-hosted Measure instance. Register, configure credentials and connect to Measure.
1. Visit your GitHub organization's settings page, located at [https://github.com/organizations/YOUR-ORGANIZATION/settings/profile](https://github.com/organizations/YOUR-ORGANIZATION/settings/profile). Replace `YOUR-ORGANIZATION` with the name of your organization.
2. Locate **Developer Settings** at the bottom of the left sidebar and click on **OAuth Apps**
3. Click the **New Org OAuth App** button
4. Enter a name for your GitHub OAuth app
5. Enter the homepage URL, like: [https://measure.yourcompany.com](https://measure.yourcompany.com). Replace `yourcompany.com` with your domain.
6. Enter a suitable description of your app
7. Enter the following as the **Authorization callback URL** - [https://measure.yourcompany.com/auth/callback/github](https://measure.yourcompany.com/auth/callback/github). Replace `yourcompany.com` with your domain.
8. Click on **Register application** button to create the GitHub OAuth app
9. Click on **Generate a new client secret**
10. Copy the **Client ID** and paste when asked in prompt for `Enter GitHub OAuth app key`
11. Copy the **Client Secret** and paste when asked in prompt for `Enter GitHub OAuth app secret`
---
Source: https://measure.sh/docs/hosting/smtp-email
---
# Set up SMTP email
Configure an SMTP email provider for your self-hosted Measure instance to send invites, alerts and daily summary emails.
Set up an email provider to get SMTP credentials. We recommend [Ethereal Mail](https://ethereal.email) for local development/testing and [Resend](https://resend.com), [SendGrid](https://sendgrid.com) or [AWS SES](https://aws.amazon.com/ses) for production.
Your email provider should let you configure the email domain that invite and alert notifications will use as the "from" address and give you the other SMTP credentials needed for the following steps.
If you do not provide an email domain, by default, Measure will use the SITE\_ORIGIN varaiable where the dashboard is deployed as the "from" address.
## Configure SMTP email settings for existing users \[#configure-smtp-email-settings-for-existing-users]
If you are upgrading from v0.7.x, you would need to manually configure the SMTP settings.
1. Edit the `self-host/.env` file.
2. Add the following environment variables as obtained from your email provider.
```sh
SMTP_HOST=smtp.yourprovider.email # change this
SMTP_PORT=587 # change this
SMTP_USER=user@yourprovider.email # change this
SMTP_PASSWORD=some_secret_password # change this
EMAIL_DOMAIN=your_email_domain.com # change this
```
3. Run the following command to shutdown all services.
```sh
sudo docker compose \
-f compose.yml \
-f compose.prod.yml \
--profile migrate \
down
```
4. Finally, run the `install.sh` script for the configuration to take effect.
```sh
sudo ./install.sh
```
---
Source: https://measure.sh/docs/hosting/slack
---
# Set up Slack
Set up Slack integration for your self-hosted Measure instance. Configure the Slack app with OAuth, slash commands and events for crash alerts and the query agent.
Use this guide to setup Slack integration to receive Measure alert notifications on Slack and to ask the Measure query agent questions from Slack.
## Configure Slack settings for new installation \[#configure-slack-settings-for-new-installation]
1. **Slack App**. Create a Slack app following the official [Slack guide](https://docs.slack.dev/quickstart/). You may choose any name, logo and description you wish for your app.
2. **Basic Information**. Go to `Basic Information` section and copy client id, client secret and signing secret and paste them into the prompts. (If you're upgrading an existing Measure installation you will paste these variables into your environment variables file. See section for existing users below)
3. **OAuth & Permissions**. Go to the `OAuth & Permissions` section of your app and under `Redirect URLs`, add your Measure Slack authentication callback URL. This should be something like `https://[measure.yourcompany.com]/auth/callback/slack`. Replace **`\[measure.yourcompany.com]\`\*\* with your actual Measure Dashboard domain.
4. In the same `OAuth & Permissions` section of your app, under `Scopes`, request the following permissions:
* **app\_mentions:read**
* **assistant:write**
* **chat:write**
* **chat:write.public**
* **channels:read**
* **channels:history**
* **groups:read**
* **groups:history**
* **im:history**
* **im:write**
* **commands**
* **files:write**
* **links:read**
* **links:write**
* **reactions:read**
* **reactions:write**
* **users:read**
* **users:read.email**
The steps below need the **Measure API domain** and not the Measure Dashboard domain.
If this URL is incorrect, you'll get a `dispatch_failure` error when connecting your Measure Team to your Slack Workspace.
Assuming your API domain is something like `measure-api.yourcompany.com`, you should put in `https://[measure-api.yourcompany.com]/slack/events` in the below steps.
Replace \*\*`[measure-api.yourcompany.com]`\*\* with your actual Measure API domain.
5. **Event Subscriptions**. Go to the `Event Subscriptions` section, enable events and set the Request URL to `https://[measure-api.yourcompany.com]/slack/events`. Slack verifies the URL immediately, so your Measure API service must be reachable when you save it. Then, under `Subscribe to bot events`, add:
* **app\_mention** — questions asked by @mentioning the bot in channels
* **app\_home\_opened** — lets the bot greet the user with suggested prompts when they open its DM
* **message.channels** — follow-up messages in a public channel thread, so a conversation can continue without re-mentioning the bot
* **message.groups** — the same for private channels
* **message.im** — questions asked in the bot's DMs
6. **Agent**. Go to the `Agent` section of your app settings and enable it (new Slack apps use Slack's Agent messaging experience by default). This gives the Measure app a direct-message surface where users chat with the agent, in addition to @mentioning it in channels.
7. **Slash Commands**. Go to `Slash Commands` section. Create the commands as follows:
| Command | Request URL | Short Description |
| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------ |
| /subscribe-alerts | https\://\[measure-api.yourcompany.com]/slack/events | Registers current channel to receive alert notifications |
| /stop-alerts | https\://\[measure-api.yourcompany.com]/slack/events | Stops current channel from receiving alert notifications |
| /list-alert-channels | https\://\[measure-api.yourcompany.com]/slack/events | Lists channels currently registered to receive alert notifications |
Replace \*\*\[measure-api.yourcompany.com]\*\* with your actual Measure API domain in each Request URL.
## Configure Slack settings for existing installation \[#configure-slack-settings-for-existing-installation]
If you already set up Slack integration on an earlier version of Measure, your Slack app predates the query agent and is missing the scopes, event subscriptions and settings the agent needs. Update your existing Slack app, and if you are upgrading from v0.8.2 or below, your environment variables, as described below.
Slack cannot send event and OAuth callbacks to your local dev environments. In order to run and test Slack integration while developing or testing locally, you will need to use a tunneling service such as [ngrok](https://ngrok.com) or [Cloudflare tunnels](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/) and use that URL to proxy to your localhost.
See this [guide](https://docs.slack.dev/tools/node-slack-sdk/tutorials/local-development/#using-a-local-request-url-for-development) for more information.
### Update your Slack app \[#update-your-slack-app]
1. **New scopes**. Open your existing Slack app at [api.slack.com/apps](https://api.slack.com/apps), go to the `OAuth & Permissions` section and, under `Scopes`, add the following permissions while keeping the ones you already have:
* **app\_mentions:read**
* **assistant:write**
* **chat:write.public**
* **channels:history**
* **groups:history**
* **im:history**
* **im:write**
* **files:write**
* **links:read**
* **links:write**
* **reactions:read**
* **reactions:write**
* **users:read**
* **users:read.email**
Your app should now have the full set of scopes listed in step 4 of the [new installation guide](#configure-slack-settings-for-new-installation) above.
2. **Event Subscriptions**. This section did not exist for alerts-only installs, so you are adding it for the first time. Follow step 5 of the [new installation guide](#configure-slack-settings-for-new-installation) to enable events, set the Request URL and subscribe to the bot events.
3. **Agent**. Follow step 6 of the [new installation guide](#configure-slack-settings-for-new-installation) to enable the agent.
4. **Reinstall the app**. New scopes only take effect after the app is reinstalled. Once you add them, Slack shows a banner prompting you to reinstall. Open the `Install App` section and reinstall the app to your workspace. Your existing bot token keeps working and gains the new scopes, so you do not need to reconnect Slack from the Measure dashboard.
### Update environment variables \[#update-environment-variables]
If you are upgrading from `v0.8.2` or below, you also need to add the Slack credentials to your environment variables manually instead of entering them at a terminal prompt. If you are upgrading from `v0.10.0`, you need to rename `SLACK_OAUTH_STATE_SALT` environment variable to `SLACK_OAUTH_STATE_SECRET`.
1. **Slack Client Credentials**. Open the `self-host/.env` file & add the following environment variables as obtained from your Slack app page.
```sh
SLACK_CLIENT_ID=your-slack-client-id # change this
SLACK_CLIENT_SECRET=your-slack-client-secret # change this
SLACK_SIGNING_SECRET=your-slack-signing-secret # change this
```
2. **State Secret**. Generate a random 44 character secret. Use the command `openssl rand -hex 22` to generate a random secret.
```sh
SLACK_OAUTH_STATE_SECRET=your-slack-oauth-state-secret # change this
```
3. **Shutdown**. Run the following command to shutdown all services.
```sh
sudo docker compose -f compose.yml -f compose.prod.yml --profile migrate down
```
4. **Startup**. Finally, run the `install.sh` script for the configuration to take effect.
```sh
sudo ./install.sh
```
---
Source: https://measure.sh/docs/hosting/agent
---
# Set up Measure Agent
Configure Measure Agent on your self-hosted instance. Get an OpenRouter API key, choose the models and expose the agent service so you can debug your app from your coding agent or Slack.
Use this guide to set up [Measure Agent](https://measure.sh/docs/agent) on a self-hosted instance, so you can debug your app from your coding agent (over MCP) or from Slack.
The agent runs as a separate `agent` service and sends prompts to a language model through [OpenRouter](https://openrouter.ai). You provide an OpenRouter API key and choose which models the agent uses.
## Get an OpenRouter API key \[#get-an-openrouter-api-key]
1. Create an account at [openrouter.ai](https://openrouter.ai).
2. Add credits to your account on the [Credits](https://openrouter.ai/credits) page. OpenRouter bills the agent's usage per token, so the account needs a positive balance for the agent to answer questions.
3. Create an API key on the [Keys](https://openrouter.ai/keys) page and copy it. You will set it as `LLM_AGENT_KEY`.
4. Optionally, create a second key for the Ask AI chat on the docs pages. You will set it as `LLM_DOCS_CHAT_KEY`. Give this key a credit limit, so traffic from the public docs pages cannot spend the agent's credits. Skip it to keep the docs chat off.
## Choose the models \[#choose-the-models]
The agent uses two models:
* **Small model** (`LLM_AGENT_MODEL_SMALL`) handles light work: summarizing long conversations and working out which app a Slack question is about. It does not call tools, so any capable chat model works. Pick a fast, inexpensive one.
* **Medium model** (`LLM_AGENT_MODEL_MEDIUM`) answers the questions by calling Measure's tools, so it **must support tool (function) calling**. Pick a stronger model, since this is where most of the answer quality comes from.
Both models should have a context window of at least 64k tokens so they can hold a long conversation comfortably.
You can use the same model for both, as long as it supports tool calling. Browse the available models at [openrouter.ai/models](https://openrouter.ai/models), confirm the medium model lists tool (function) calling support, and use the model's API id, for example `deepseek/deepseek-v4-pro`.
`LLM_AGENT_MODEL_LARGE` is accepted for forward compatibility but is not used yet. You can leave it empty.
## Configure for a new installation \[#configure-for-a-new-installation]
During installation, the configuration wizard prompts for the agent settings. At the **agent LLM credentials and models** prompts, paste your API key and the small and medium model ids. The wizard writes `AGENT_ENABLED=false`, so once installation finishes, turn the agent on as described in [Turn the agent on](#turn-the-agent-on).
The wizard also asks for the **Measure Agent service URL**, for example `https://measure-agent.yourcompany.com`. This is the public address coding agents use to reach the MCP endpoint. See [Make the agent reachable over MCP](#make-the-agent-reachable-over-mcp).
## Configure for an existing installation \[#configure-for-an-existing-installation]
If your instance is already running and you want to enable or change the agent, update the environment variables manually.
1. **Agent Credentials**. Open the `self-host/.env` file & add the following environment variables. Set the credentials and models as obtained from OpenRouter, and set `AGENT_ENABLED=true` to turn the agent on.
```sh
AGENT_ENABLED=true
LLM_AGENT_KEY=your-openrouter-api-key # change this
LLM_AGENT_MODEL_SMALL=deepseek/deepseek-v4-pro # change this
LLM_AGENT_MODEL_MEDIUM=deepseek/deepseek-v4-pro # change this
LLM_DOCS_CHAT_KEY=your-docs-chat-api-key # optional, turns on the docs Ask AI chat
```
2. **Shutdown**. Run the following command to shutdown all services.
```sh
sudo docker compose -f compose.yml -f compose.prod.yml --profile migrate down
```
3. **Startup**. Finally, run the `install.sh` script for the configuration to take effect.
```sh
sudo ./install.sh
```
## Turn the agent on \[#turn-the-agent-on]
The agent is off by default. It answers only when `AGENT_ENABLED` is set to `true` in `self-host/.env`; with any other value, or with the variable missing, Slack questions and the MCP `ask_question` tool receive a short notice that the agent is unavailable instead of an answer. The other MCP tools read data directly without the agent, so they keep working.
To turn the agent on, set the variable and restart the services as described above.
```sh
AGENT_ENABLED=true
```
Set it back to `false` and restart to take the agent out of service without removing its configuration.
## Make the agent reachable over MCP \[#make-the-agent-reachable-over-mcp]
To use the agent from a coding agent over MCP, the `agent` service (port `8084`) needs a public domain. This uses the same reverse-proxy and DNS setup as the dashboard, API and ingest services in the [Self-Hosting Guide](https://measure.sh/docs/hosting).
Add a reverse proxy entry for the agent domain. For Caddy, add the following to your `~/Caddyfile`.
```
measure-agent.yourcompany.com {
reverse_proxy http://localhost:8084
}
```
Reload the proxy, then add a DNS A record pointing `measure-agent.yourcompany.com` at your VM's external IP, the same way you did for the other subdomains.
The MCP endpoint is then available at `https://measure-agent.yourcompany.com/mcp`. See the [MCP Server](https://measure.sh/docs/mcp) guide for connecting coding agents.
Using the agent from Slack needs no extra domain: Slack delivers events to the API service and the agent consumes them internally. Set that up with the [Slack integration guide](https://measure.sh/docs/hosting/slack).
---
Source: https://measure.sh/docs/hosting/migration-guides
---
# Migration guides
Migration guides for upgrading self-hosted Measure across versions that need extra configuration or data migration.
Use these guides when upgrading to newer versions of Measure that requires some additional configurations, like one time data maintenance operations. For the usual upgrade process, [follow this guide](https://measure.sh/docs/hosting#upgrade-a-self-hosted-installation).
Some migrations may contain *optional* steps. The specific guide would state that clearly.
## List of migration guides \[#list-of-migration-guides]
* Choose as per your target version.
* These are bridge versions, so if you are upgrading from an ancient version, make sure to run the migration guide for each version in order.
* [**v0.4.x**](https://measure.sh/docs/hosting/migration-guides/v0.4.x) - Migration Guide for `v0.4.x`
* [**v0.6.x**](https://measure.sh/docs/hosting/migration-guides/v0.6.x) - Migration Guide for `v0.6.x`
* [**v0.8.x**](https://measure.sh/docs/hosting/migration-guides/v0.8.x) - Migration Guide for `v0.8.x`
* [**v0.9.x**](https://measure.sh/docs/hosting/migration-guides/v0.9.x) - Migration Guide for `v0.9.x`
* [**v0.10.x**](https://measure.sh/docs/hosting/migration-guides/v0.10.x) - Migration Guide for `v0.10.x`
* [**v0.12.x**](https://measure.sh/docs/hosting/migration-guides/v0.12.x) - Migration Guide for `v0.12.x`
---
Source: https://measure.sh/docs/hosting/migration-guides/v0.4.x
---
# Migration Guide for `v0.4.x`
Migration guide for self-hosted Measure v0.4.x. Optional drop of old data and required data backfills.
Use this guide only when you are on less than `v0.4.0` and upgrading to `v0.4.x`.
Steps mentioned in this document will cause downtime.
## Upgrade while optionally dropping old data \[#upgrade-while-optionally-dropping-old-data]
Follow these steps to drop all older sessions **before upgrading**.
Follow these steps when upgrading to `0.4.x`. There is some downtime involved. During the downtime SDKs would receive a `503 Service Unavailable` when sending sessions. Once the upgrade is complete, ingestion should resume normally. SDKs will retry sending unsent sessions automatically.
### 1. SSH into the VM where Measure is hosted \[#1-ssh-into-the-vm-where-measure-is-hosted]
### 2. Bring down services \[#2-bring-down-services]
Skip to step 4 if you **do not want to delete old data**
```sh
cd measure/self-host
sudo docker compose down api cleanup
```
### 3. Run the following commands to drop all older sessions data \[#3-run-the-following-commands-to-drop-all-older-sessions-data]
Skip to step 4 if you **do not want to delete old data**
```sh
sudo docker compose exec clickhouse clickhouse-client --progress -q "truncate table events;"
sudo docker compose exec postgres psql -U postgres -c "truncate table unhandled_exception_groups, anr_groups, event_reqs;"
```
### 4. Perform the upgrade \[#4-perform-the-upgrade]
Visit [Releases](https://github.com/measure-sh/measure/releases) page to capture the latest tag matching the `[MAJOR].[MINOR].[PATCH]` format.
```sh
cd ~/measure
git fetch --tags
git checkout
cd self-host
sudo docker compose -f compose.yml -f compose.prod.yml \
--profile init \
--profile migrate \
down
sudo docker compose pull
sudo ./install.sh
```
### 5. Run data backfills \[#5-run-data-backfills]
Perform this step regardless of whether you chose to drop data or not. Certain features on the Measure dashboard like filters, metrics and some graphical plots won't show otherwise.
This may take some time. Make sure your SSH connection remains active until it completes.
```sh
sudo ./migrations/v0.4.x-data-backfills.sh
```
---
Source: https://measure.sh/docs/hosting/migration-guides/v0.6.x
---
# Migration Guide for `v0.6.x`
Migration guide for self-hosted Measure v0.6.x. Required data backfills for user-defined attributes.
Use this guide only when you are on less than `v0.6.0` and upgrading to `v0.6.x`.
Steps mentioned in this document will cause downtime.
Follow these steps when upgrading to `0.6.x`. There is some downtime involved. During the downtime SDKs would receive a `503 Service Unavailable` when sending sessions. Once the upgrade is complete, ingestion should resume normally. SDKs will retry sending unsent sessions automatically.
## 1. SSH into the VM where Measure is hosted \[#1-ssh-into-the-vm-where-measure-is-hosted]
## 2. Perform the upgrade \[#2-perform-the-upgrade]
Visit [Releases](https://github.com/measure-sh/measure/releases) page to capture the latest tag matching the `[MAJOR].[MINOR].[PATCH]` format.
```sh
cd ~/measure
git reset --hard # only applies if you have local modifications
git fetch --tags
git checkout
cd self-host
sudo ./install.sh
```
## 3. Run data backfills \[#3-run-data-backfills]
Perform this step to complete the migration. Certain features on the Measure dashboard like user defined attributes won't work otherwise.
This may take some time. Make sure your SSH connection remains active until it completes.
```sh
sudo ./migrations/v0.6.x-data-backfills.sh
```
---
Source: https://measure.sh/docs/hosting/migration-guides/v0.8.x
---
# Migration Guide for `v0.8.x`
Migration guide for self-hosted Measure v0.8.x. Required data backfills for crashes and ANRs.
Use this guide only when you are on less than `v0.8.0` and upgrading to `v0.8.x`.
Steps mentioned in this document will cause downtime.
Follow these steps when upgrading to `0.8.x`. There is some downtime involved. During the downtime SDKs would receive a `503 Service Unavailable` when sending sessions. Once the upgrade is complete, ingestion should resume normally. SDKs will retry sending unsent sessions automatically.
## 1. SSH into the VM where Measure is hosted \[#1-ssh-into-the-vm-where-measure-is-hosted]
## 2. Perform the upgrade \[#2-perform-the-upgrade]
Visit [Releases](https://github.com/measure-sh/measure/releases) page to capture the latest tag matching the `[MAJOR].[MINOR].[PATCH]` format.
```sh
cd ~/measure
git reset --hard # only applies if you have local modifications
git fetch --tags
git checkout
cd self-host
sudo ./install.sh
```
## 3. Run data backfills \[#3-run-data-backfills]
Perform this step to complete the migration. Certain features on the Measure dashboard like crashes and ANRs won't work otherwise.
This may take some time. Make sure your SSH connection remains active until it completes.
```sh
sudo ./migrations/v0.8.x-backfills.sh
```
---
Source: https://measure.sh/docs/hosting/migration-guides/v0.9.x
---
# Migration Guide for `v0.9.x`
Migration guide for self-hosted Measure v0.9.x. Configuration migration and database synchronization.
Use this guide only when you are on less than `v0.9.0` and upgrading to `v0.9.x`.
Steps mentioned in this document will cause downtime.
Follow these steps when upgrading to `0.9.x`. There is some downtime involved. During the downtime SDKs would receive a `503 Service Unavailable` when sending sessions. Once the upgrade is complete, ingestion should resume normally. SDKs will retry sending unsent sessions automatically.
## 1. SSH into the VM where Measure is hosted \[#1-ssh-into-the-vm-where-measure-is-hosted]
## 2. Shutdown all Measure services \[#2-shutdown-all-measure-services]
```sh
cd ~/measure/self-host
```
```sh
sudo docker compose -f compose.yml -f compose.prod.yml --profile init --profile migrate down --remove-orphans
```
## 3. Perform the upgrade \[#3-perform-the-upgrade]
Visit [Releases](https://github.com/measure-sh/measure/releases) page to capture the latest tag matching the `[MAJOR].[MINOR].[PATCH]` format.
```sh
cd ~/measure
```
```sh
git reset --hard # only applies if you have local modifications
```
```sh
git fetch --tags
```
```sh
git checkout
```
## 4. Migrate configurations \[#4-migrate-configurations]
```sh
cd self-host
```
```sh
sudo ./config.sh --production --ensure
```
## 5. Run database synchronization & migration scripts \[#5-run-database-synchronization--migration-scripts]
Perform this step to complete the migration. Measure dashboard may not work properly until this step is completed.
```sh
sudo ./migrations/v0.9.x-sync-databases.sh
```
## 6. Start Measure services \[#6-start-measure-services]
```sh
sudo ./install.sh
```
---
Source: https://measure.sh/docs/hosting/migration-guides/v0.10.x
---
# Migration Guide for `v0.10.x`
Migration guide for self-hosted Measure v0.10.x. Adds a Google OAuth client secret, a dedicated ingest endpoint and configuration migration.
Use this guide only when you are on less than `v0.10.0` and upgrading to `v0.10.x`.
Steps mentioned in this document will cause downtime.
Follow these steps when upgrading to `0.10.x`. There is some downtime involved. During the downtime SDKs would receive a `503 Service Unavailable` when sending sessions. Once the upgrade is complete, ingestion should resume normally. SDKs will retry sending unsent sessions automatically.
## 1. Shutdown all Measure services \[#1-shutdown-all-measure-services]
SSH into the VM where Measure is hosted.
```sh
cd ~/measure/self-host
```
```sh
sudo docker compose -f compose.yml -f compose.prod.yml --profile init --profile migrate down --remove-orphans
```
## 2. Perform the upgrade \[#2-perform-the-upgrade]
Visit [Releases](https://github.com/measure-sh/measure/releases) page to capture the latest tag matching the `v[MAJOR].[MINOR].[PATCH]` format. For example, `v0.10.0`.
```sh
cd ~/measure
```
```sh
git reset --hard # only applies if you have local modifications
```
```sh
git fetch --tags
```
```sh
# replace with the chosen tag. example: v0.10.0
git checkout
```
## 3. Create Google OAuth client secret \[#3-create-google-oauth-client-secret]
Skip this step if you only use **GitHub** sign in.
Starting with `v0.10.x`, Google sign-in uses a server-side code flow that requires the `OAUTH_GOOGLE_SECRET` environment variable. Previously, only the client ID (`OAUTH_GOOGLE_KEY`) was needed.
1. Go to [Google Cloud Console](https://console.cloud.google.com) > APIs & Services > Credentials
2. Click on your existing OAuth 2.0 Client ID, create a new **Client Secret** and copy it. (If you want to disable the existing client secret for security reasons and are sure it is not being used anywhere else outside of Measure, you can do so.)
3. Edit `self-host/.env` and add:
```sh
OAUTH_GOOGLE_SECRET=your-google-client-secret # change this
```
## 4. Set up an ingest endopint \[#4-set-up-an-ingest-endopint]
Set up a new DNS A record for the new ingest endpoint like: `https://measure-ingest.yourcompany.com` pointing to your VM's IP. This step is new and recommended to ensure the best possible ingestion performance. Make sure to also update this endpoint as the `API_URL` in your apps. All the app versions prior to this change will continue to ingest as well.
## 5. Migrate configurations \[#5-migrate-configurations]
```sh
cd ~/measure/self-host
```
```sh
sudo ./config.sh --production --ensure
```
You'll be prompted to enter the ingest endpoint you created in step 4 above.
## 6. Start Measure services \[#6-start-measure-services]
```sh
sudo ./install.sh
```
## 7. Run data back filling script \[#7-run-data-back-filling-script]
Perform this step to complete the migration. Measure dashboard will not work properly until these scripts are run.
```sh
sudo ./migrations/v0.10.x-data-backfills-1.sh
```
```sh
sudo ./migrations/v0.10.x-data-backfills-2.sh
```
```sh
sudo ./migrations/v0.10.x-read-optim.sh
```
---
Source: https://measure.sh/docs/hosting/migration-guides/v0.12.x
---
# Migration Guide for `v0.12.x`
Migration guide for self-hosted Measure v0.12.x. Modifies Slack integration, a dedicated MCP/Agent endpoint & configuration migration.
Use this guide only when you are on less than `v0.12.0` and upgrading to `v0.12.x`.
Steps mentioned in this document will cause downtime.
Follow these steps when upgrading to `0.12.x`. There is some downtime involved. During the downtime SDKs would receive a `503 Service Unavailable` when sending sessions. Once the upgrade is complete, ingestion should resume normally. SDKs will retry sending unsent sessions automatically.
## 1. Shutdown all Measure services \[#1-shutdown-all-measure-services]
SSH into the VM where Measure is hosted.
```sh
cd ~/measure/self-host
```
```sh
sudo docker compose -f compose.yml -f compose.prod.yml --profile init --profile migrate down --remove-orphans
```
## 2. Perform the upgrade \[#2-perform-the-upgrade]
Visit [Releases](https://github.com/measure-sh/measure/releases) page to capture the latest tag matching the `v[MAJOR].[MINOR].[PATCH]` format. For example, `v0.12.0`.
```sh
cd ~/measure
```
```sh
git reset --hard # only applies if you have local modifications
```
```sh
git fetch --tags
```
```sh
# replace with the chosen tag. example: v0.12.0
git checkout
```
## 3. Migrate configurations \[#3-migrate-configurations]
```sh
cd ~/measure/self-host
```
```sh
sudo ./config.sh --production --ensure
```
You'll be prompted to enter the agent endpoint, enter a value like `https://measure-agent.yourcompany.com`.
## 4. Re-configure Slack \[#4-re-configure-slack]
Skip this step if you don't wish to use Slack integration.
Starting with `v0.12.x`, the Measure Slack app needs additional permissions & configurations. Visit the [Set up Slack](https://measure.sh/docs/hosting/slack#configure-slack-settings-for-existing-installation) guide for instructions.
## 5. Configure Measure Agent & MCP \[#5-configure-measure-agent--mcp]
Skip this step if you don't wish to use Measure MCP/Agent.
Starting with `v0.12.x`, the Measure MCP endpoint URL format has changed. Additionally, you can set up the Measure Agent as well. Visit the [Set up Measure Agent](https://measure.sh/docs/hosting/agent) guide for instructions. After the setup reauthorize the remote [MCP server in your coding agents](https://measure.sh/docs/mcp#connecting-to-mcp-via-coding-agents).
## 6. Start Measure services \[#6-start-measure-services]
```sh
sudo ./install.sh
```
---
Source: https://measure.sh/blog/what-we-got-wrong-about-anr-detection-before-we-got-it-right
---
# What we got wrong about ANR detection before we got it right
A deep dive into ANR detection on Android.

At Measure, we build an open source mobile observability platform. One of the trickiest things to track on Android is the dreaded Application Not Responding (ANR) error. When the UI thread of an Android app is blocked for too long, Android decides to throw this error and lets the user kill the app.

This post is about how we detects ANRs and the attempts we made before getting it right.
## Main thread watchdog \[#main-thread-watchdog]
We started with the simplest and most well known way to detect ANRs. Run a watchdog thread that periodically posts a token to the main thread’s Handler. If the token doesn’t come back within 5 seconds, track an ANR event. The implementation was entirely in Kotlin and easy to ship.
The problem was that it was flaky in practice. The watchdog would fire on hangs that Android itself wouldn’t classify as ANRs, while missing real ANRs reported by the system.
The root issue is that Android doesn’t have a single universal ANR timeout. There are different thresholds and trigger conditions. For example, input dispatch ANRs are triggered when the main thread fails to respond to input events within 5 seconds, while broadcast receivers, services, foreground service startup, content providers and `JobScheduler` interactions all have their own timeout rules.
We did not end up shipping this.
\##Using ApplicationExitInfo
From API 30 (Android 11) onwards Android itself writes a full ANR dump automatically. `ActivityManager` returns a list of [ApplicationExitInfo](https://developer.android.com/reference/android/app/ApplicationExitInfo) records describing the app’s recent process exits including ones with `REASON_ANR`. It also contains the state of every thread at the time of crash.
However, this API has a few limitations.
First, it’s API 30 and above only. We support older releases (API level 21 and above) where `ApplicationExitInfo` doesn’t exist. We needed ANR detection that works on every device our SDK runs on.
Second, it only tells you about process exits after the fact. We only see the record on the next app launch, by which point everything we’d have wanted from the moment of the ANR (for example a screenshot of the moment the ANR occurred) is gone with the process.
Third, the system keeps these records in a bounded ring buffer, and older entries get evicted as new ones come in. There’s no guarantee the specific ANR we want is still around when the app is launched again.
We use `ApplicationExitInfo` where it’s available, but cannot fully rely on it to achieve our goals.
## The real signal \[#the-real-signal]
Signals are how Unix-style operating systems poke a process when something asynchronous happens. Pressing Ctrl+C in a terminal sends `SIGINT` to the foreground process. A segmentation fault generates `SIGSEGV`. Each signal has a number (`SIGINT` is `2`, `SIGKILL` is `9`, `SIGQUIT` is `3`) and a default behavior the kernel applies if the process doesn’t override it.
For `SIGQUIT`, the default on Linux is to terminate the process and write a core dump. Android overrides this behavior. It shows the “App Not Responding” dialog and writes an ANR report.
All we needed was a way to intercept this signal, record an ANR and pass it back to the system to continue doing its thing. It was harder to do than we initially thought.
## Catching SIGQUIT \[#catching-sigquit]
The obvious first move to detect a signal is to register a signal handler. So we registered one to handle `SIGQUIT`.
```c
struct sigaction sa = { .sa_handler = on_sigquit };
sigaction(SIGQUIT, &sa, NULL);
```
In a regular Linux process this is enough to get notified of the signal.
On Android the handler never runs.
To see why, it helps to know there are two ways a thread can deal with an incoming signal.
The first is what we just did. Install a handler with `sigaction`, and when the signal arrives the kernel pauses the thread mid-instruction, runs the handler, and resumes. The catch is that the handler runs in interrupted context. You can’t allocate, take a mutex or call into the JVM. The list of things you can safely do (the [async-signal-safe](https://man7.org/linux/man-pages/man7/signal-safety.7.html) list) is short.
The second approach is to block the signal on every thread, then dedicate one thread to pulling it off the pending queue with `sigwait` or `sigwaitinfo`. The signal arrives as a return value rather than an interrupt, so the dedicated thread runs in ordinary context and can allocate, take locks, and call into the runtime.
Android picks the second pattern for `SIGQUIT`. At runtime startup, it blocks `SIGQUIT` in every thread and spawns a dedicated thread named Signal Catcher that sits in a loop calling sigwaitinfo. When `SIGQUIT` arrives, no thread has it unblocked, so the kernel has nothing to interrupt. The signal sits in the pending queue until Signal Catcher pulls it out to produce the ANR trace.
That’s why our handler never fired. Installing a handler doesn’t unblock the signal, and every thread inherited `SIGQUIT` blocked at runtime startup. The kernel had no thread to interrupt, so the signal queued, and Signal Catcher continued it’s work.

## Watchdog 2.0 \[#watchdog-20]
To get our handler running, we need our own thread in the process with `SIGQUIT` unblocked. Then `SIGQUIT` becomes deliverable, the kernel picks our thread, and the handler runs.
We spawn one thread, call it Watchdog, and unblock `SIGQUIT` for it with `pthread_sigmask`. Signal masks are per-thread, so Signal Catcher is unaffected. Watchdog is now the only thread in the process where `SIGQUIT` is unblocked, which makes it the only thread the kernel can deliver to.
The handler waits for the signal to arrives.
```c
static void on_sigquit(int sig) {
sem_post(&anr_sem);
}
```
The trick is to keep the handler as small as possible and have Watchdog do the real work after the handler returns. A semaphore makes the handoff.
Watchdog spends most of its life waiting on the semaphore. When `SIGQUIT` arrives, the kernel runs our handler on Watchdog. The handler wakes the semaphore and returns. That’s all it does, because waking a semaphore is one of the few things you can safely do from inside a signal handler.
Watchdog is now back in its own code. The handler ran on this thread, did one async-signal-safe thing, and returned. Everything past the semaphore wait is ordinary thread code, so Watchdog can take locks, allocate, call into the JVM, walk threads and capture the state we want for the ANR.
This works, but now it breaks the platform’s ANR flow.
Signal Catcher is still parked in sigwaitinfo, waiting on a signal we just consumed. No `SIGQUIT` means no “App Not Responding” dialog.
## Handing the signal back \[#handing-the-signal-back]
Watchdog needs to send a fresh `SIGQUIT` to Signal Catcher so the platform machinery continues to run.
We can’t just send another `SIGQUIT` to the process. The kernel looks for a thread with `SIGQUIT` unblocked, finds Watchdog (still the only one), and the signal comes right back to us.
We need to target Signal Catcher directly. The primitive for that is `tgkill`, which delivers a signal to a specific thread by its TID. Which we don’t have.
Getting Signal Catcher’s TID is the awkward part. There’s no API for it, but `/proc/self/task/` has a directory for every thread in the process, and each directory has a comm file with the thread’s name. At SDK init we walk the directory once, find the entry that reads “Signal Catcher”, and grab its TID. When an ANR fires we record it and send `SIGQUIT` back to Signal Catcher via \`\`tgkill\`.
The platform’s ANR flow now runs as it would have without us in the picture, except we now have our own data captured at the moment it happened.

## Putting ANRs on the timeline \[#putting-anrs-on-the-timeline]
Capturing an ANR is complex, but fixing them is even harder. We did all of this so that every ANR comes with the full picture of what led up to it.
First, a timeline of events that occurred before the ANR was triggered: HTTP requests, navigation transitions, lifecycle callbacks, gesture events and custom events.

Second, the ApplicationExitInfo record from Android along with the stack trace it provides. Third, an optional screenshot of the screen at the moment the ANR fired.
This allows scrolling back through the session to see what the user was doing right up to the moment the app froze.
You can interact with a real session replay [here](https://measure.sh/product/session-replays)
## Sources \[#sources]
The native ANR detection code lives in our Android SDK on [GitHub](https://github.com/measure-sh/measure/blob/main/android/measure-android/measure/src/main/jni/anr_handler.c). On the platform side, AOSP’s [signal\_catcher.cc](https://android.googlesource.com/platform/art/+/master/runtime/signal_catcher.cc) is the file that implements the Signal Handler thread.
The man pages for the functions and signals mentioned above are linked below for reference.
* [signal(7)](https://man7.org/linux/man-pages/man7/signal.7.html)
* [sigaction(2)](https://man7.org/linux/man-pages/man2/sigaction.2.html)
* [sigwait(3)](https://man7.org/linux/man-pages/man3/sigwait.3.html)
* [sigwaitinfo(2)](https://man7.org/linux/man-pages/man2/sigwaitinfo.2.html)
* [pthread\_sigmask(3)](https://man7.org/linux/man-pages/man3/pthread_sigmask.3.html)
* [sem\_post(3)](https://man7.org/linux/man-pages/man3/sem_post.3.html)
* [tgkill(2)](https://man7.org/linux/man-pages/man2/tgkill.2.html)
* [proc(5)](https://man7.org/linux/man-pages/man5/proc.5.html)
---
Source: https://measure.sh/blog/mobile-breaks-differently
---
# Mobile breaks differently
Mobile app issues are stateful unlike typical server side stateless request cycle errors. Your observability should reflect that.

Most observability platforms started life watching servers. They got really good at it. Tracing requests across microservices, tracking error rates per endpoint and alerting on P99 latency spikes. Mobile came later and the same architecture and data models were extended to support it.
Except mobile observability is a fundamentally different problem. The failure modes are different, the constraints are different, and if you care about monitoring your app well, it’s worth understanding how and why that matters.
## You don’t own the machine \[#you-dont-own-the-machine]
When a backend service misbehaves, you SSH in, read the logs, bump the memory, restart the process. You have full control.
On mobile, your code runs on a device in someone’s pocket, on a network you’ve never heard of, in a country you’ve never been to, on an OS version you didn’t know any one was still using.
That device might have 2 GB of RAM shared across 40 apps. It might be running Android 9 with a manufacturer skin that patches the lifecycle callbacks differently. It might be an iPhone SE on a train going through a tunnel.
This changes how you collect data, what data you collect, and what you do with it. Every byte of telemetry has to survive unreliable networks, respect battery life, and fit through bandwidth constraints, all while making sure your observability tool doesn’t itself become a performance problem.
## You can’t revert an app release \[#you-cant-revert-an-app-release]
Backend deploys are reversible. Something breaks, you roll back or quickly patch a fix, the whole cycle ideally takes minutes. You can ship ten times a day.
Mobile releases go through app store review. That’s hours at best, days at worst. And even after your fix is approved, users have to actually update. Some won’t for weeks. Some never will.
The life of a bug that ships is just dramatically higher. A backend bug is a bad hour. A mobile bug can be a bad week or even a month. You need to spot the problem forming early in a release cycle while your rollout is still small or pay a higher price later.
## Crashes aren’t errors \[#crashes-arent-errors]
A 500 error on a server is bad, but the server restarts or just keeps running. The next request probably works fine.
A crash kills the session. The user was in the middle of something (placing an order, writing a message) and now they’re staring at their home screen. There’s no automatic retry. There’s just a person deciding whether your app is worth opening again.
Then there are ANRs (Application Not Responding). The app hasn’t crashed, it’s technically still alive, but it’s frozen and the OS is asking the user if they want to force close. There’s really no backend equivalent. It’s one of the most frustrating experiences a mobile user can have, and a lot of observability tools don’t even track it properly.
## Stack traces are useless without symbolication \[#stack-traces-are-useless-without-symbolication]
When you ship a mobile app, you don’t ship the code you wrote. Release builds go through optimisation and obfuscation - ProGuard or R8 on Android, symbol stripping on iOS. Your carefully named PaymentProcessor.processTransaction() becomes something like a.b.c() on Android or a hex memory address on iOS.
This is good for app size and security. It’s terrible for debugging. When a crash comes in from production, the stack trace is gibberish. A wall of obfuscated class names and stripped addresses that tells you nothing about what actually went wrong.
To make it readable again, you need symbolication: mapping those mangled names and addresses back to your original source code. On iOS, that means dSYM files generated at build time. On Android, it’s ProGuard or R8 mapping files. Every build produces its own mapping, and if you lose it or upload the wrong one, your crash reports are permanently unreadable for that version.
This is an operational burden that just doesn’t exist in backend. Your server logs say NullPointerException in PaymentService.java:142 and you go fix it. For mobile, you need a pipeline that automatically captures mapping files for every build, matches them to the right app version, and symbolicates crash reports as they come in. Get any step wrong and you’re staring at 0x0000000100a3b2c4 wondering what went sideways.
It’s one of those things that’s invisible when it works and completely debilitating when it doesn’t.
## A request is not a session \[#a-request-is-not-a-session]
Backend observability is built around the request. A request comes in, gets traced across services, produces a response. Clean and well-bounded.
Mobile users don’t make requests. They have sessions. They open the app, tap around, switch to another app, come back twenty minutes later, scroll, hit a button, get interrupted by a phone call, return, and eventually close the app. Or don’t, it just gets killed by the OS when memory runs low.
Understanding what went wrong means reconstructing that journey. What screens did they visit? What did they tap? What network calls fired? What was the memory pressure at the time? What did the screen actually look like right before the crash?
A timestamped error log tells you almost nothing. You need the full session replay: navigation events, gestures, network calls, resource usage, UI state and much more context to see what actually happened.
## Performance is relative \[#performance-is-relative]
When a backend engineer talks about performance, they mean latency and throughput on known hardware. You know exactly what you’re working with.
Mobile performance might mean cold start time, warm start time, time to first frame, frame rendering jank, memory consumption, battery drain, or app size. And every one of these varies across devices. Your app starts in 400ms on a Pixel 9 and 4 seconds on a budget Samsung from 2020. Both are real users.
If your observability tool only shows you averages, you’re seeing a number that represents nobody. You need to slice by device, OS version, app version, network type, geography. You need the distribution, not the mean.
## The latency you don’t see \[#the-latency-you-dont-see]
Backend tracing follows a request across services. A request comes in, hops through some microservices, and produces a response. The trace has a clear start and end.
Mobile traces are messier. A trace might span multiple screens as a user works through a flow. Adding items to a cart, entering an address, hitting checkout. They might pause halfway through to reply to a text, or lose connectivity on the subway, or get a phone call. The app backgrounds, the OS might reclaim memory, and the user may or may not come back.
Then there’s the network side. Your server dashboard says the API responded in 200ms but the user waited three seconds. The gap is everything that happened before the request reached your server: DNS resolution on a flaky network, TLS handshake on a slow connection, request queuing while the cellular radio wakes up.
Backend traces pick up at the API gateway. Everything before that is invisible unless your mobile tooling captures it.
Mobile-aware tracing connects both sides, the on-device spans and the backend spans, so when a user says “the app felt slow,” you can actually tell whether the problem was the network, the client, or your API.
## The telemetry paradox \[#the-telemetry-paradox]
A hard part of mobile observability it that the thing you’re measuring is the thing being affected by the measurement.
Every event you log takes CPU, memory, and battery. Every network request to ship telemetry uses bandwidth the user might be paying for. A heavy SDK that captures everything will make the app slower and drain more battery, creating the exact problems you’re trying to detect. It doesn’t matter much if a tracing sidecar uses an extra 200MB of RAM on a server. On a phone, your SDK’s overhead is a direct tax on user experience.
Mobile SDKs have to be absurdly efficient. Batch intelligently, compress aggressively, back off when resources are tight. Capture enough to be useful but little enough to be invisible.
## The fragmentation nightmare \[#the-fragmentation-nightmare]
“Works on my device” is the mobile version of “works on my machine,” except it’s orders of magnitude worse.
There are thousands of distinct Android devices in active use — different screen sizes, chipsets, GPU capabilities, RAM configurations, manufacturer customizations, OS forks. iOS is more constrained but still spans multiple hardware generations and OS versions.
A bug might only reproduce on Samsung devices running Android 12 with a specific GPU driver. Or on iPhone SE in low power mode. Or only when the app is restored from background after 30 minutes on a slow network.
You see a 0.5% crash rate and shrug, but that might be 100% of users on a specific device having an awful time.
## A different beast \[#a-different-beast]
Mobile observability isn’t backend observability with a different client library. It’s a related but different discipline with it’s own primitives and fundamentally different failure modes.
The tooling that your mobile team depends on should recognise and reflect that.
---
Source: https://measure.sh
---
# Mobile apps break, get to the root cause faster.
Measure helps mobile teams monitor and fix crashes, ANRs, bugs, and performance issues. The open source alternative to **Firebase Crashlytics**.
## Trusted by high growth mobile teams
Kuku FM, Hoichoi, Country Delight, Dashreels, Turtlemint, Astro, Allofresh, SMC India, Even, Karya.
## One dashboard, Complete context
## Intelligent debugging, Seamless integration
Debug with [Measure Agent](/product/agent) right inside Slack or your coding agent. Ask about a crash, error or slow endpoint and it digs through your telemetry to find the answer.
Connect Measure with your favorite coding agents through our [MCP Server](/product/mcp). Let your coding agent query errors, traces and session replays directly in your development workflow.
## Collect what you need, Only when you need it
Most monitoring data rots away in a warehouse and runs up your costs 💰. Our [Adaptive Capture](/product/adaptive-capture) feature lets you control and dynamically change what data to collect without needing to roll out app updates.
## Tried it, Loved it ❤️
> I've been using measure.sh lately to monitor my mobile apps and host it myself and it has been a delight. Definitely recommend it to anyone looking for an open source mobile app monitoring tool.
>
> — Hussain Mustafa ([source](https://x.com/husslingaround/status/1855983892294983980))
> I'm surprised this hasn't gained more attention yet — it's incredibly exciting for the mobile space, where we definitely lack observability and measure addresses so many of those gaps.
>
> — Aditya Pahilwani ([source](https://x.com/AdityaPahilwani/status/1843561672188821520))
> When I stumbled upon measure.sh, I was blown away! Crash-free sessions improved dramatically — now hitting a mythical 99.99% consistently. Logs, metrics, traces — finally stitched together in one view. Our hot & warm app startup times? Looking great!
>
> — Sutirth Chakravarty ([source](https://www.linkedin.com/posts/sutirthchakravarty_circa-early-2024-i-had-the-chance-to-attend-activity-7317570327520124928-yo1s))
> The good folks at measure.sh have been working on a mobile app monitoring platform for several months now and have open-sourced it. Do check it out and show it some love! This is quite a strong team that led several mobile platform initiatives at Gojek.
>
> — Ragunath Jawahar ([source](https://x.com/ragunathjawahar/status/1825490936857522290))
> I'm personally a fan. Not just of the product, but of the minds behind it. It's built by some of the sharpest mobile engineers I've admired for years. Folks who live and breathe performance, scaling, and observability. This isn't just another tool. It's crafted with intent, care, and deep expertise.
>
> — Iniyan Murugavel ([source](https://www.linkedin.com/posts/iniyanarul_crashes-were-observed-first-on-measure-activity-7316853914589413377-gFd_/))
> Looking for a way to keep tabs on your mobile apps? How about using a free and open-source solution? Consider exploring measure.sh!
>
> — Tuist ([source](https://www.linkedin.com/posts/tuistio_github-measure-shmeasure-measure-is-an-activity-7312413362292719616-DUlU))
## Built For Mobile Devs
For us, Mobile is not an add-on to an observability product. It **is** the product. Measure is built by mobile engineers, for mobile engineers.
- **Open Source** — [Star us on GitHub](https://github.com/measure-sh/measure).
- **Simple Pricing** — Pay only for the [data you use](/pricing). No seat limits.
- **Every mobile platform** — Android, iOS, Flutter, React Native (soon).
Get started:
---
Source: https://measure.sh/about
---
# For mobile engineers, by mobile engineers
We built Measure to solve the unique challenges mobile developers face in monitoring production apps.
After spending years in the trenches building mobile apps at scale, we understood that existing tools that are often web and backend centric don't address mobile-specific needs.
For us, mobile is not an add-on to an observability product. Mobile **is** the product.
We strongly believe that tools for mobile developers can and should be better and that's what drives us everyday.
## Team
- **[Gandharva Kumar](https://www.linkedin.com/in/gandharvakr/)** — CEO
- **[Anup Cowkur](https://www.linkedin.com/in/anupcowkur/)** — CTO
- **[Abhay Sood](https://www.linkedin.com/in/abhaysood/)** — Head of Mobile
- **[Debjeet Biswas](https://www.linkedin.com/in/debjeet-biswas-9b4337281/)** — Head of Infra
- **[Adwin Ross](https://www.linkedin.com/in/adwin-ronald-ross/)** — Mobile Engineer
## Investors
Picus Capital · DeVC · Astir Ventures
## Angels
- Mustafa Ali — Head of Mobile, Shopify
- Kunal Shah — Founder, CRED
- Misbah Ashraf — Co-Founder, Jar
- Vatsal Singhal — Co-Founder, Ultrahuman
- Anshuman Bajoria — Strategy and Operations, Revolut
- Anuj Bhagat — Product, Google
- Sudhanshu Raheja — President, GoTo Financial
- Sidu Ponnappa — CEO, realfast
- Abhinit Tiwari — Head of Design, Gojek
- Ranjan Sakalley — Co-Founder, base14
- Gaurav Batra — Co-Founder, Semaai
- Paul Meinshausen — CEO, Aampe
Get started:
---
Source: https://measure.sh/bugsnag-alternative
---
# Looking for Bugsnag alternatives?
Bugsnag is an established error monitoring and app stability tool covering mobile alongside web and backend across dozens of platforms.
Measure is a mobile first, open source Bugsnag alternative.
## Full session context on every issue
Bugsnag gives you stack traces with breadcrumb trails of what happened before the error. Breadcrumbs have a max limit and there is no visual replay of the session.
Measure attaches a full [Session Replay](/product/session-replays) with gestures, navigation, network calls, lifecycle events and custom spans to every crash, ANR and error, with no hard limit on what you can see.
You see exactly what the user did and what the app did, on every issue, without any compromise on the context.
## Adaptive capture, not quota sampling
Bugsnag keeps you limited to the tier you pay for by sampling. Performance data is sampled server-side so it fits your span quota, and errors are metered against a monthly event quota. In case of traffic spikes or sudden user growth, you would end up with less visibility into your system when you need more.
Measure captures full session context by default, and with [Adaptive Capture](/product/adaptive-capture) you can tune what you collect remotely, without shipping an app update.
Dial up on new releases or when chasing tricky production issues, dial down whenever you need to.
## Fully open source
Bugsnag publishes its notifier SDKs on GitHub under the MIT license, but the backend and dashboard are proprietary. You can read the SDK but the rest of the platform is opaque.
Measure is [fully open source](https://github.com/measure-sh/measure). Read it, run it, self-host it, audit the pipeline and if you think something can be done better, send a pull request.
## Simple, predictable pricing
Bugsnag meters two separate things, error events and performance spans, each against its own monthly quota. Exceeding quotas means sampling or overage.
Measure has a single, transparent [price](/pricing) based on how much data you use. No separate product meters. With [Adaptive Capture](/product/adaptive-capture) you can dial collection up or down without rolling out app updates to control your costs even better.
## Built for mobile, by mobile devs
Bugsnag monitors mobile, web and backend across 50+ platforms and is now one product inside SmartBear's larger testing and monitoring suite. Mobile is one player among many, and the defaults, platform decisions, dashboards and roadmap are shaped by the whole portfolio rather than by the needs of mobile devs alone.
Measure is built only for mobile. [Crashes & ANRs](/product/crashes-and-anrs), [App Health](/product/app-health), [Performance Traces](/product/performance-traces), [Network Performance](/product/network-performance), [Bug Reports](/product/bug-reports) and [User Journeys](/product/user-journeys) are all designed around how mobile apps actually break in production.
Mobile is not a part of our product, it is the whole product.
## Measure vs Bugsnag
| Capability | Measure | Bugsnag |
| --- | --- | --- |
| Crash reporting with full session replays | ✓ | Crash reports with limited breadcrumbs |
| ANR detection with full session replays | ✓ | ANRs with limited breadcrumbs |
| Performance traces | ✓ | ✓ |
| Network monitoring | ✓ | ✓ |
| User journeys | ✓ | ✗ |
| In-app bug reports | ✓ | ✗ |
| Session replay on every issue | ✓ | Limited breadcrumbs only |
| Dynamic Sampling with Adaptive Capture | ✓ | Quota-driven sampling |
| Auto-captured context | Gestures, navigation, network, lifecycle | Navigation, network, taps via limited breadcrumbs |
| Pricing | Simple pricing based on data usage | Separate quotas for error events & performance spans |
| Open Source | Apache 2.0 (OSI open source) | SDKs only |
| Self-hostable | ✓ | Enterprise on-premise |
| Public roadmap & issue tracker | ✓ | SDK repos only |
| Mobile focus | ✓ | One of many platforms |
Get started:
---
Source: https://measure.sh/crashlytics-alternative
---
# The open-source Firebase Crashlytics alternative, built for mobile
Measure gives mobile teams crashes, ANRs, performance, network monitoring and full session context in one thoughtful platform. Every issue gets an auto-captured [Session Replay](/product/session-replays), so you and your coding agents have the deep context needed to fix issues fast. Measure is fully open-source and gives you complete control over your data with no sampling.
## Why mobile teams look for a Firebase Crashlytics alternative
Crashlytics is free, widely deployed and a sensible place to start. For most apps, its basic crash reporting is enough to get going. Teams tend to start looking due to the following reasons:
1. **Limited context makes solving issues harder.** A stack trace tells you where the app crashed, but doesn't tell you what the user and device were doing when it happened. Crashlytics requires manually instrumenting breadcrumbs and keeping them in sync with every release. Individually instrumenting every possible user interaction, device signal, network event and navigation change is cumbersome and hard to keep up with as the app evolves. Teams often find out in production that they are missing logs and events which could have helped them debug issues quicker.
2. **No control over sampling.** To keep crash reporting and performance monitoring free, Firebase applies internal sampling which developers cannot change. Production issues are affected by device, network, app versions, OS versions and many other factors. The ability to collect and analyze data across multiple dimensions dynamically is necessary to hone in on issues as apps scale.
3. **Toolset Fragmentation hides the true cost.** Performance traces go in Firebase Performance Monitoring, a separate product with a separate SDK. Analytics events which are useful for debugging end up in Google Analytics. Custom analysis of your own data needs paid BigQuery export and only happens in delayed batches. Custom alerting needs Cloud Functions. In-app bug reports require a third-party tool. The number of SDKs in your app, the dashboards you look at and the MCP integrations your agents need keep climbing, with the context you need for any single investigation spread across multiple sources.
4. **Platform Lock-In.** The Crashlytics SDKs are open source, but the backend and dashboard are proprietary. You cannot audit the code, verify the data pipeline, or move your raw data out to any destination except BigQuery with a paid export.
Measure was built to close these gaps: full session context by default, dynamic sampling with user control, one platform for everything mobile teams need, and an open stack you can contribute to.
## Measure vs Firebase Crashlytics: The Full Comparison
| Capability | Measure | Firebase Crashlytics |
| --- | --- | --- |
| Crash reporting | Yes, with Session Replay on every crash | Yes, with manually instrumented breadcrumbs |
| ANR detection | Yes, with Session Replay attached | Yes, with manually instrumented breadcrumbs |
| Session context on every issue | Auto-captured | Manual breadcrumbs |
| Session Replay | Yes, on every issue | ✗ |
| Auto-captured context | Gestures, navigation, network calls, lifecycle events | Screen views when Google Analytics is enabled; rest is manual |
| Network monitoring | Yes, with full dynamic sampling control | Separate Firebase Performance Monitoring product, with no user-controlled sampling |
| Performance traces | Yes, with full dynamic sampling control | Separate Firebase Performance Monitoring product, with no user-controlled sampling |
| In-app bug reports | ✓ | No, needs a third-party tool |
| User journeys | ✓ | Requires Google Analytics |
| Open source | Yes, Apache 2.0 end to end | SDKs only; backend and dashboard are proprietary |
| Self-hostable | ✓ | ✗ |
| Public roadmap and issue tracker | ✓ | SDK repositories only |
| Raw data export | To any destination, in Enterprise plans | Paid export to BigQuery only |
| Platforms | Android, iOS, iPadOS, Flutter, React Native, Kotlin Multiplatform | Apple platforms, Android, Flutter, Unity |
| Product focus | Mobile only | One of many Firebase products |
## Go Beyond crash reports with full session context
A Crashlytics crash report gives you a stack trace and whatever breadcrumbs you instrumented ahead of time. Taps, navigations, network calls and lifecycle transitions each need their own instrumentation, and this needs to keep up with code changes resulting in an error prone process with missing context as the app evolves.
Measure auto-captures gestures, navigation, lifecycle events, network calls and traces, then replays them as a [Session Replay](/product/session-replays) attached to every crash, ANR and error.
The debugging process changes from guesses about what happened to facts you can observe. Instead of reading a stack trace, forming a hypothesis, shipping breadcrumbs and waiting a release cycle to test it, you just open the issue and watch what happened. Even better, just point your agent at the issue and it can use our [MCP Server](/product/mcp) to fetch deep context across several occurrences to help you find the root cause. The hardest to reproduce issues: a crash that only happens on a certain device in a specific navigation path, an error that only occurs when a background request times out before completion, a failure that depends on state built up over several screens, become easier than ever to fix.
## Performance, Crash and ANR monitoring built only for mobile
Crashlytics is part of Firebase, where mobile is one product line among many and roadmap decisions compete with everything else on the platform.
Measure is built only for mobile. [Crashes & ANRs](/product/crashes-and-anrs), [App Health](/product/app-health), [Performance Traces](/product/performance-traces), [Network Performance](/product/network-performance), [Bug Reports](/product/bug-reports) and [User Journeys](/product/user-journeys) are all designed around the failure modes mobile apps experience in production: memory pressure, main-thread blocking, errors during background and foreground transitions, and unstable network conditions.
Mobile is not a part of our product. It is the whole product.
## The self-hostable, open-source Crashlytics alternative
Crashlytics publishes its SDKs on GitHub, but the backend and dashboard are proprietary and run only on Google's infrastructure.
Measure is [open source end to end](https://github.com/measure-sh/measure) under an Apache 2.0 license. You can read the code, run it, self-host it, and audit how data is collected and stored. If you have ideas on how to make it better, you can open an issue or send a pull request.
Open source software is better for transparency because you can see the code handling your data. It's better for security since more eyes on the code lead to more discovered vulnerabilities. It is better for flexibility, since you can raise issues and PRs to improve the platform or host it yourself if you have the need. Being open source also makes Measure easier to use with coding agents - just point your agent at the code or docs and it can figure out everything it needs to make full use of everything the platform offers without poking around a black box.
## One platform for Android, iOS, iPadOS, Flutter, React Native and KMP
Measure supports [Android](/for/android), [iOS](/for/ios), [iPadOS](/for/ipados), [Flutter](/for/flutter), [React Native](/for/react-native) and [Kotlin Multiplatform](/for/kmp).
Our SDKs are designed to be thoughtful, flexible, lightweight and performant across all platforms. Crashes, ANRs, performance traces, network monitoring and session context are tracked, symbolicated and collected with platform-specific best practices in mind so that observability doesn't impact the performance of the app itself.
Data across all your Android, iOS and cross-platform apps, along with their dev, staging and production variants feeds into a single unified dashboard so you can ship and monitor your apps with confidence.
## Simple, transparent pricing with full data ownership
Crashlytics crash reporting is free but data export and advanced analysis depend on separate products with independent pricing. Products like BigQuery export for custom analysis, Cloud Functions for custom alerting, Google analytics for user interaction events lead to platform lock-in and hard to predict costs as apps scale.
Measure has a single [price](/pricing) based on how much data you send. No per-seat charges, no arbitrary feature bundles. Raw data export is available in enterprise plans to a destination you choose without restriction to a particular cloud or vendor. With [Adaptive Capture](/product/adaptive-capture) you can adjust data collection rates without shipping an app update, which makes it easy to scale telemetry when your app needs to while keeping costs under control.
## Who is Measure right for?
Measure is useful for any mobile app but it fits best for apps with growing users, complexity and scale. If production issues are getting harder to debug due to missing information about the states that led to them, or if users are complaining about performance and network issues and your current setup lacks deep telemetry and context to fix them, Measure will fit like a glove.
Measure can also be a good choice if data ownership, auditability of the platform and avoiding platform lock-in to a single ecosystem matters to you for security or compliance reasons.
If simple crash reporting is all you need, and your team is already comfortable inside the Google ecosystem, Crashlytics is a decent option. Measure is designed for growing mobile teams that need production observability at scale. With deep telemetry, Measure makes fixing issues with agents and shipping amazing mobile experiences easier and faster.
## Migrating from Crashlytics
Switching to Measure does not have to be a rip-and-replace. You can install the Measure SDK and run it alongside Crashlytics while you evaluate. A generous free tier lets you integrate your app, send telemetry data, use session replays, performance traces and MCP server integration to debug issues and see how Measure helps improve your app.
Many teams use both Crashlytics and Measure together until they make the switch. Setup and per-platform guides are in the [docs](/docs).
## Firebase Crashlytics alternative FAQs
### Is there an open-source alternative to Firebase Crashlytics?
Yes. Measure is a fully open-source alternative to Firebase Crashlytics, licensed under Apache 2.0. Crashlytics publishes its SDKs as open source, but its backend and dashboard are proprietary. Measure's entire stack is open, so you can read the code, self-host it and audit how data is collected and stored. It covers crashes, ANRs, performance, network monitoring and session context, and is built only for mobile.
### Is Firebase Crashlytics open source?
Partially. The Crashlytics SDKs are open source on GitHub, but the backend and dashboard are closed and run only on Google's infrastructure. That means you cannot self-host Crashlytics, run its servers yourself, or audit the full ingestion pipeline. If an end-to-end open-source stack matters to your team, Measure is 100% open source.
### Is Firebase Crashlytics free?
Yes. Crashlytics crash reporting is free to use. Costs start when you go further: Exporting your data to BigQuery for custom analysis and Cloud Functions for custom alerting are separately billed services. Measure has a single usage-based price and generous free tier to get you started.
### Is Measure free?
Measure has a generous free tier which is sufficient for most small teams and solo developers. For teams hitting scale, we offer a pro plan with a simple usage-based pricing.
### Does Firebase Crashlytics report ANRs?
Yes, for Android apps. Crashlytics collects ANRs and attaches breadcrumbs if you've taken the time to manually instrument them. Measure reports ANRs with a Session Replay attached, so you can see the user interactions and device activity that lead to them making debugging easier.
### Can Measure replace Firebase Crashlytics?
Yes, it can. Measure covers the core Crashlytics job of crash and ANR reporting, and adds session context, network monitoring, performance traces and in-app bug reports in the same SDK. You can run both side by side during evaluation. Teams that only need free crash reporting inside the Google ecosystem may still prefer Crashlytics but for teams looking for advanced mobile performance monitoring and issue debugging, Measure offers a better platform.
### Does Measure support Android, iOS, Flutter, React Native and Kotlin Multiplatform?
Yes. Measure has SDKs for Android, iOS, iPadOS, Flutter, React Native and Kotlin Multiplatform. Crashes, ANRs, performance traces, network monitoring and session context all feed into one dashboard, so cross-platform teams can monitor and debug in one unified tool.
### Does Measure support Claude Code, Codex, Pi, OpenCode and other coding agents?
Yes. Measure has an MCP server that is specifically designed to give your coding agents deep app context so they can help you fix issues faster. You can also set up automated workflows such as loops to have your agents fix issues on their own using the MCP integration.
### Can Measure be self-hosted?
Yes. Because Measure is open source under Apache 2.0, you can self-host the entire stack, backend and dashboard included, on infrastructure you control. Crashlytics cannot be self-hosted, since its backend is proprietary. Self-hosting keeps crash and real-user session data in your own environment, which certain terms need. Our hosted cloud option is a better option for most teams who would rather not manage and scale the platform themselves.
Or checkout the [docs](/docs).
Get started:
---
Source: https://measure.sh/datadog-alternative
---
# Looking for Datadog alternatives?
Datadog is a comprehensive observability platform with roots in infrastructure and backend monitoring, spanning servers, cloud, APM, logs, security, web and mobile.
Measure is a mobile first, open source Datadog alternative.
## Full session context on every issue
Datadog gives you stack traces and auto-captured events out of the box with Mobile Session Replays billed separately. If you want full context on every error, you will need to turn on Mobile Session Replays for all of them and accommodate the significant cost increase.
Measure attaches a full [Session Replay](/product/session-replays) with gestures, navigation, network calls, lifecycle events and custom spans to every crash, ANR and error and you only pay for the data used as a whole.
You see exactly what the user did and what the app did, on every issue, without any compromise on the context.
## Adaptive capture, not fixed sampling
Datadog uses client-side sampling. You set a session sample rate, with a separate replay sample rate applied on top and decide up front what fraction to keep.
Measure captures full session context by default, and with [Adaptive Capture](/product/adaptive-capture) you can tune what you collect remotely, without shipping an app update.
Dial up sample rates on new releases or when chasing tricky production issues, dial down whenever you need to.
## Fully open source
Datadog publishes its mobile SDKs as open source, but the backend and dashboard are proprietary. You can read the SDK, but you can't see or run the platform that ingests and stores your data.
Measure is [fully open source](https://github.com/measure-sh/measure). Read it, run it, self-host it, audit the pipeline and if you think something can be done better, send a pull request.
## Simple, predictable pricing
Datadog is metered across a long list of separate SKUs. RUM sessions are split into tiers, Mobile Session Replay is billed on top, and per-host APM, infrastructure and logs have their own price lists.
Measure has a single, transparent [price](/pricing) based on how much data you use. No per-seat fees, no separate product meters. With [Adaptive Capture](/product/adaptive-capture) you can dial collection up or down without rolling out app updates to control your costs even better.
## Built for mobile, by mobile devs
Datadog monitors infrastructure, servers, cloud, APM, logs, security and frontend across hundreds of integrations. Mobile is one small corner of a sprawling observability platform, and the defaults, dashboards, product decisions and roadmap are shaped by the whole platform rather than by the needs of mobile devs alone.
Measure is built only for mobile. [Crashes & ANRs](/product/crashes-and-anrs), [App Health](/product/app-health), [Performance Traces](/product/performance-traces), [Network Performance](/product/network-performance), [Bug Reports](/product/bug-reports) and [User Journeys](/product/user-journeys) are all designed around how mobile apps actually break in production.
Mobile is not a part of our product, it is the whole product.
## Measure vs Datadog
| Capability | Measure | Datadog |
| --- | --- | --- |
| Crash reporting with full session replays | ✓ | Crash reports with auto-captured events, session replay sampled & billed separately |
| ANR detection with full session replays | ✓ | ANRs, session replay sampled & billed separately |
| Performance traces | ✓ | ✓ |
| Network monitoring | ✓ | ✓ |
| User journeys | ✓ | ✓ |
| In-app bug reports | ✓ | ✗ |
| Session replay on every issue | ✓ | Session replay, sampled & billed separately |
| Dynamic Sampling with Adaptive Capture | ✓ | Client side only sampling |
| Auto-captured context | Gestures, navigation, network, lifecycle | Actions, views, network, errors |
| Pricing | Simple pricing based on data usage | Separate SKUs for RUM session tiers, replay, APM, infra & logs |
| Open Source | Apache 2.0 (OSI open source) | SDKs only |
| Self-hostable | ✓ | ✗ |
| Public roadmap & issue tracker | ✓ | SDK repos only |
| Mobile focus | ✓ | One small part of a huge platform |
Get started:
---
Source: https://measure.sh/embrace-alternative
---
# Looking for Embrace alternatives?
Embrace is a mobile and web observability platform that offers crash reporting, ANR tracking, network monitoring and performance traces.
Measure is a mobile first, open source Embrace alternative.
## Full session context on every issue
Embrace and Measure both attach a full session view to every crash, ANR and error and capture it automatically.
Measure records gestures, navigation, network calls, lifecycle events and custom spans into a full [Session Replay](/product/session-replays) on every issue.
The key difference is transparency. Measure is open source, the platform that stores your user data is transparent, and you never have to send your data to a proprietary, locked platform.
## Adaptive capture, not all-or-nothing
Measure and Embrace both allow you to capture full session data without sampling. Where they differ is control: Embrace captures everything and bills per session, so full context means paying for every session your app generates which can be significant at scale.
Measure captures full session context, but with [Adaptive Capture](/product/adaptive-capture) you can tune what you collect remotely, without shipping an app update.
Dial up on a new release, dial down to cut cost or noise. You decide how much you collect, and change it whenever you need to.
## Fully open source
Embrace open sources its SDKs but the backend and dashboard that ingest, store and surface your data are locked behind a proprietary platform with no auditability.
Measure is [fully open source](https://github.com/measure-sh/measure). Run the entire stack yourself, audit the pipeline end to end, keep your data on your own infrastructure if you choose, and if something can be done better, send a pull request.
## Simple, predictable pricing
Embrace charges per session which means a session with barely any activity matters the same as one with lots of interactions.
Measure has a single, transparent [price](/pricing) based on how much data you actually ingest which is a much more practical metric as it relates directly to usage of the platform without meaningless sessions costing more than they need to. With [Adaptive Capture](/product/adaptive-capture) you can also tune collection anytime to keep costs in check.
## Built for mobile, by mobile devs
Embrace supports mobile and web monitoring. Mobile is one of the supported platforms, and the defaults, platform decisions, dashboards and product roadmap are shaped by the whole platform rather than by the needs of mobile devs alone.
Measure is mobile first and focused on mobile developers. [Crashes & ANRs](/product/crashes-and-anrs), [App Health](/product/app-health), [Performance Traces](/product/performance-traces), [Network Performance](/product/network-performance), [Bug Reports](/product/bug-reports) and [User Journeys](/product/user-journeys) are all shaped only by how mobile apps break in production.
Mobile is not a part of our product, it is the whole product.
## Measure vs Embrace
| Capability | Measure | Embrace |
| --- | --- | --- |
| Crash reporting with full session replays | ✓ | ✓ |
| ANR detection with full session replays | ✓ | ✓ |
| Performance traces | ✓ | ✓ |
| Network monitoring | ✓ | ✓ |
| User journeys | ✓ | ✓ |
| In-app bug reports | ✓ | ✗ |
| Session replay on every issue | ✓ | ✓ |
| Dynamic Sampling with Adaptive Capture | ✓ | Always-on full capture |
| Auto-captured context | Gestures, navigation, network, lifecycle | Taps, views, network, lifecycle |
| Pricing | Simple pricing based on data usage | Per session |
| Open Source | Apache 2.0 (OSI open source) | SDKs only |
| Self-hostable | ✓ | ✗ |
| Public roadmap & issue tracker | ✓ | SDK repos only |
| Mobile focus | ✓ | Mobile and Web |
Get started:
---
Source: https://measure.sh/for/android
---
# Measure for Android
Measure is an open source, mobile first monitoring platform built for Android. Measure gives you all the context you need to decrease crash rates, increase app performance and deliver smoother experiences for your Android app users.
## Session Replays
Every crash and ANR in your Android app arrives with a full [Session Replay](/product/session-replays). Replay the exact sequence of events that led to the issue — gestures, navigation, network calls, logs and lifecycle events — with CPU and memory signals right alongside.
Stop guessing from a stack trace and see exactly what the user and the app did leading up to the moment things went wrong.
## Detailed Stack Traces
Every [crash and ANR](/product/crashes-and-anrs) comes with a full stack trace captured across every thread, so you can figure out what each thread was doing, not just the one that threw the error.
Stack traces are automatically deobfuscated, mapping minified R8 and ProGuard output back to your original class and method names with their intact line numbers. Mapping files are automatically uploaded by our Gradle plugin so you can focus on fixing issues and let Measure handle the boring stuff.
## Performance Monitoring
Instrument the operations that matter most with [Performance Traces](/product/performance-traces). See how API fetches, database calls, expensive code paths and screen rendering stack up within a single user flow or across millions of sessions with waterfall charts that make bottlenecks obvious.
Traces carry rich device and app context linking back to full session replays, so you can tie slow operations to the environment they happened in.
## App Health
Stay on top of every release with [App Health](/product/app-health). Track app adoption, crash-free and ANR-free sessions, error rates as your users actually perceive them, app size, and launch times across cold, warm and hot starts.
Spot a bad rollout early and fix it before it reaches the rest of your users.
## Bug Reports
Let users report problems the moment they see them with [Bug Reports](/product/bug-reports), triggered by a device shake or a call to the SDK from your own button. Each report captures device information, app version, network conditions and screenshots alongside the user's description, and links straight to the complete session replay.
Skip the email threads and support ticket back-and-forth. Your users describe the issue in their own words and you get all the context you need to solve it.
## User Journeys
See the real paths users take through your app with [User Journeys](/product/user-journeys). Every screen transition is mapped automatically into clear flow diagrams, and the exception view shows exactly where issues interrupt those flows.
Short on time and figuring out what issues to prioritize? Easily see which paths are important to users so you can unblock them first.
## Network Monitoring
Watch every request your app makes with [Network Performance](/product/network-performance). See HTTP status code distributions over time and drill into your top endpoints ranked by latency, error rate and request frequency to find the calls slowing your app down.
Catch degraded endpoints early and optimize the API calls that matter most to your users.
## Coding Agents
Bring all of Measure's context into your favorite coding agents. The [Measure MCP server](/product/mcp) gives any coding agent access to your crashes, ANRs, performance traces and session replays, straight from your IDE, editor or terminal.
Ask it to help you debug a crash, analyze user sessions or use it to set up an agentic issue triage and debug pipeline. Whether you prefer commercial tools or open source agents and models, Measure fits right into your workflows.
Works great with Claude Code, OpenAI Codex, Google Antigravity, Cursor, OpenCode, Pi, Devin, Kilo Code, Cline, Roo Code and others.
Get started:
---
Source: https://measure.sh/for/flutter
---
# Measure for Flutter
Measure is an open source, mobile first monitoring platform built for Flutter. It brings together the full context behind every crash and error across your Dart and native code, so you can drive down crash and error rates, smooth out performance issues and deliver a delightful experience on both Android and iOS.
## Session Replays
Every crash and error in your Flutter app comes with a complete [Session Replay](/product/session-replays) you can replay. Step back through the exact lead-up — gestures, navigation, network calls, logs and lifecycle events — with CPU and memory readings plotted right beside them.
Instead of reading an out-of-context Dart stack trace, you can see exactly what the user did and how the app responded just before things went wrong.
## Detailed Stack Traces
Every [crash and error](/product/crashes-and-anrs) carries a complete Dart stack trace, alongside any native crash from the Android or iOS side.
Stack traces are automatically deobfuscated, mapping both native and Dart code to your original class and method names with their intact line numbers. Let Measure deal with the tedious part so you can focus on debugging issues.
## Performance Monitoring
Wrap the operations that matter in [Performance Traces](/product/performance-traces). See how network requests, platform channels, expensive widget builds and rendering stack up within a single flow or across millions of sessions with waterfall charts that make the slow parts obvious.
Every trace comes with full device and app context and ties back to its session replay, so a slow span never shows up without the conditions that produced it.
## App Health
Keep a close eye on every release with [App Health](/product/app-health). Follow adoption, crash-free sessions, user perceived error rates, app size, and launch times across your Android and iOS builds.
Notice a buggy rollout early and fix it before it spreads to the rest of your users.
## Bug Reports
Let users report a problem the second they notice it with [Bug Reports](/product/bug-reports), triggered by a device shake or from your own button through the SDK. Each report packages device details, app version, network conditions and a screenshot next to the user's description, and makes it easy to jump straight to the matching session replay.
Forget the email threads and support ticket back-and-forth — your users explain the issue in their own words and you get every bit of context needed to resolve it.
## User Journeys
Follow the paths people take through your production app with [User Journeys](/product/user-journeys). Every screen transition is charted automatically into clear flow diagrams, and the exception view marks exactly where issues derail those flows.
Deciding what to fix first? See which routes carry the most users so you can clear the most frequent blockers.
## Network Monitoring
See every request your app makes with [Network Performance](/product/network-performance). Follow how HTTP status codes shift over time and drill into your heaviest endpoints, ranked by latency, error rate and call volume, to find the requests slowing down your app.
Catch failing endpoints early and tune the API calls your users depend on most.
## Coding Agents
Bring Measure's full context into the coding agents you already work with. The [Measure MCP server](/product/mcp) hands any agent your crashes, errors, performance traces and session replays, directly from your IDE, editor or terminal.
Point it at a crash, work through user sessions, or build it into an agentic triage and debugging pipeline. Whether you prefer commercial tools or open source agents and models, Measure drops straight into your workflow.
Works great with Claude Code, OpenAI Codex, Google Antigravity, Cursor, OpenCode, Pi, Devin, Kilo Code, Cline, Roo Code and others.
Get started:
---
Source: https://measure.sh/for/ios
---
# Measure for iOS
Measure is an open source, mobile first monitoring platform built for iOS. Measure gives you the full context behind every crash and slowdown, so you can decrease crashes and errors, improve performance and deliver a smoother experience to your iOS app users.
## Session Replays
Every crash and error in your iOS app gets a complete [Session Replay](/product/session-replays) attached. Step back through everything that led up to it — taps and gestures, screen navigation, network calls, logs and lifecycle events — with CPU and memory readings plotted right beside them.
Instead of working backwards from a lone stack trace, you can see exactly what the user did and how the app responded in the moments before things broke.
## Detailed Stack Traces
Every [crash report](/product/crashes-and-anrs) comes with a full stack trace captured across every thread, so you can see what each one was doing, not just the thread that crashed.
Traces are symbolicated automatically, turning raw memory addresses back into the original function names, files and line numbers from your Swift and Objective-C sources. Upload your dSYMs through the Xcode build phase or straight from your .xcarchive and let Measure handle the symbolication so you can stay focused on the fix.
## Performance Monitoring
Put traces around the operations you care about with [Performance Traces](/product/performance-traces). Watch how network requests, disk and database work, heavy code paths and screen rendering add up inside a single user flow or across millions of sessions with waterfall charts that make the slow parts jump out.
Each trace carries detailed device and app context and links back to the full session replay, so a slow operation always comes with the conditions it ran under.
## App Health
Keep a close eye on every release with [App Health](/product/app-health). Follow adoption, crash-free sessions, the error rates your users actually perceive, app size, and launch times across cold, warm and hot starts.
Catch a bad rollout while it's still contained and fix it before it reaches the rest of your users.
## Bug Reports
Let users flag problems the instant they hit them with [Bug Reports](/product/bug-reports), triggered by a device shake or from your own button through the SDK. Every report bundles device details, app version, network conditions and a screenshot together with the user's note, and links straight to the matching session replay.
No more long email threads or support ticket ping-pong. Users describe the issue in their own words while you get all the context needed to fix it.
## User Journeys
Trace the actual routes people take through your app with [User Journeys](/product/user-journeys). Screen-to-screen movement is mapped for you into clear flow diagrams, and the exception view shows you where issues degrade those flows.
Not sure what to tackle first? See at a glance which paths matter most to your users so you can prioritize effectively.
## Network Monitoring
Keep tabs on every request your app fires with [Network Performance](/product/network-performance). Track how HTTP status codes trend over time and dig into your busiest endpoints, ranked by latency, error rate and call volume, to surface the requests dragging your app down.
Spot failing endpoints early and tune the API calls that matter most to your users.
## Coding Agents
Pull all of Measure's context into the coding agents you already use. The [Measure MCP server](/product/mcp) opens up your crashes, performance traces and session replays to any agent, right from your IDE, editor or terminal.
Have it dig into a crash, walk through user sessions, or wire it into an agentic triage and debugging pipeline. Whether you prefer commercial tools or open source agents and models, Measure slots straight into your workflow.
Works great with Claude Code, OpenAI Codex, Google Antigravity, Cursor, OpenCode, Pi, Devin, Kilo Code, Cline, Roo Code and others.
Get started:
---
Source: https://measure.sh/for/ipados
---
# Measure for iPadOS
Measure is an open source, mobile first monitoring platform with full support for iPadOS. It surfaces the complete context behind every crash and performance issue to help you cut crash and error rates, sharpen performance and keep your iPad app feeling effortless.
## Session Replays
Every crash and error on iPad comes with a complete [Session Replay](/product/session-replays) you can replay. Walk back through the exact run-up to the failure — gestures, screen navigation, network calls, logs and lifecycle events — with CPU and memory readings plotted right alongside.
A stack trace only tells you where things broke. The replay shows how your app got there and what the user was doing in the moments before failure.
## Detailed Stack Traces
Every [crash report](/product/crashes-and-anrs) carries a complete, multi-threaded stack trace, so you can inspect what each thread was up to, not just the one that failed.
Measure symbolicates them for you, mapping raw memory addresses back to the original function names, files and line numbers in your Swift and Objective-C code. Upload your dSYMs through the Xcode build phase or straight from your .xcarchive and let Measure worry about the symbolication so you can stay focused on the fix.
## Performance Monitoring
Wrap the operations that matter in [Performance Traces](/product/performance-traces). See how network requests, disk and database access, expensive code paths and the rendering of those larger iPad layouts accumulate within a single flow or across millions of sessions with waterfall views that make the slow parts obvious.
Every trace comes with full device and app context and ties back to its session replay, so a slow span never shows up without the conditions that produced it.
## App Health
Track the health of every release in one place with [App Health](/product/app-health). Monitor adoption, error rates, launch times and more core app metrics in one unified view.
Notice a shaky rollout early and patch it before it spreads to the rest of your users.
## Bug Reports
Let people report a problem the second they run into it with [Bug Reports](/product/bug-reports), triggered by a shake or from a button you wire up through the SDK. Each one packages device details, app version, network conditions and a screenshot next to the user's own description, and allows you to jump straight to the matching session replay.
Forget the back-and-forth of email and support tickets. Your users explain the issue in their own words and you get every bit of context needed to resolve it.
## User Journeys
Follow the real paths people take through your app with [User Journeys](/product/user-journeys). Every screen transition is charted automatically into clear flow diagrams, and the exception view marks exactly where issues derail those flows.
Deciding what to fix first? See which routes carry the most users so you can unblock the busiest ones ahead of the rest.
## Network Monitoring
See every request your app makes with [Network Performance](/product/network-performance). Follow how HTTP status codes shift over time and drill into your heaviest endpoints ranked by latency, error rate and call volume to find the requests degrading your app performance.
Catch endpoints going bad early and take care of the API calls your users depend on most.
## Coding Agents
Bring Measure's full context into the coding agents you already work with. The [Measure MCP server](/product/mcp) hands any agent your crashes, performance traces and session replays, directly from your IDE, editor or terminal.
Point it at a crash, have it work through user sessions, or build it into an agentic triage and debugging pipeline. Whether you prefer commercial tools or open source agents and models, Measure drops straight into your workflow.
Works great with Claude Code, OpenAI Codex, Google Antigravity, Cursor, OpenCode, Pi, Devin, Kilo Code, Cline, Roo Code and others.
Get started:
---
Source: https://measure.sh/for/kmp
---
# Measure for Kotlin Multiplatform
Measure is an open source, mobile first monitoring platform built for Kotlin Multiplatform. It brings together the full context behind every crash and error across your shared Kotlin code and platform code, so you can drive down crash and error rates, smooth out performance issues and deliver a delightful experience on both Android and iOS.
## Session Replays
Every crash and error in your Kotlin Multiplatform app comes with a full [Session Replay](/product/session-replays). Follow the sequence of events that led to the issue — gestures, navigation, network calls, logs and lifecycle events — with CPU and memory signals right alongside.
Go beyond the stacktrace and see exactly what the user and the app did leading up to the moment things went wrong.
## Detailed Stack Traces
Every [crash and error](/product/crashes-and-anrs) comes with a full stack trace captured across every thread, including frames from your shared Kotlin code so you know where the crash happened.
Stack traces are deobfuscated on Android and symbolicated on iOS automatically, so you read your original Kotlin classes, methods and line numbers instead of minified or raw output. Mapping files are uploaded automatically, so you can let Measure handle the boring stuff and focus on fixing user issues.
## Performance Monitoring
Instrument the operations that matter most with [Performance Traces](/product/performance-traces). See how API fetches, database calls, expensive code paths and screen rendering stack up within a single user flow or across millions of sessions with waterfall charts that make bottlenecks obvious.
Traces carry rich device and app context linking back to full session replays, so you can tie slow operations to the environment they happened in.
## App Health
Stay on top of every release with [App Health](/product/app-health). Track app adoption, crash-free sessions, error rates as your users actually perceive them, app size, and launch times across cold, warm and hot starts.
Spot a bad rollout early and fix it before it reaches the rest of your users.
## Bug Reports
Let users report problems the moment they see them with [Bug Reports](/product/bug-reports), triggered by a device shake or a call to the SDK from your own button. Each report captures device information, app version, network conditions and screenshots alongside the user's description, and links straight to the complete session replay.
Skip the email threads and support ticket back-and-forth. Your users describe the issue in their own words and you get all the context you need to solve it.
## User Journeys
See the real paths users take through your app with [User Journeys](/product/user-journeys). Every screen transition is mapped automatically into clear flow diagrams, and the exception view shows where issues interrupt those flows.
Short on time and figuring out what issues to prioritize? Easily see which paths are important to users so you can unblock them first.
## Network Monitoring
Watch every request your app makes with [Network Performance](/product/network-performance). See HTTP status code distributions over time and drill into your top endpoints ranked by latency, error rate and request frequency to find the calls slowing your app down.
Catch degraded endpoints early and optimize the API calls that matter most to your users.
## Coding Agents
Bring all of Measure's context into your favorite coding agents. The [Measure MCP server](/product/mcp) gives any coding agent access to your crashes, errors, performance traces and session replays, straight from your IDE, editor or terminal.
Ask it to help you debug a crash, analyze user sessions or use it to set up an agentic issue triage and debug pipeline. Whether you prefer commercial tools or open source agents and models, Measure fits right into your workflows.
Works great with Claude Code, OpenAI Codex, Google Antigravity, Cursor, OpenCode, Pi, Devin, Kilo Code, Cline, Roo Code and others.
Get started:
---
Source: https://measure.sh/for/react-native
---
# Measure for React Native
Measure is an open source, mobile first monitoring platform built for React Native. Whether you use vanilla React Native or Expo, Hermes or JavaScriptCore, Measure gives you the full context behind every error across your JavaScript and native layers, so you can cut crash rates, tighten performance and ship a smoother experience on both Android and iOS.
## Session Replays
Every error in your React Native app arrives with a complete [Session Replay](/product/session-replays). Replay the exact path to the issue — gestures, navigation, network calls, logs and lifecycle events — with CPU and memory readings right alongside.
Rather than piecing together a minified stack trace, you see exactly what the user did and how the app behaved in the moments before it broke.
## Detailed Stack Traces
Every [crash and error](/product/crashes-and-anrs) comes with a complete stack trace. JavaScript errors are symbolicated from your sourcemaps, so you read your own functions, files and line numbers instead of minified output, and crashes from the native Android and iOS layers are captured and mapped too.
Sourcemaps and native mapping files are uploaded automatically, so you can let Measure take care of the boring stuff and focus on fixing user issues.
## Performance Monitoring
Put traces around the operations that matter with [Performance Traces](/product/performance-traces). See how network requests, native modules, expensive JavaScript and screen rendering stack up within a single user flow or across millions of sessions with waterfall charts that make bottlenecks obvious.
Each trace carries rich device and app context and links back to the full session replay, so a slow operation always comes with the environment it ran in.
## App Health
Stay on top of every release with [App Health](/product/app-health). Track adoption, crash-free sessions, app size, and launch times for your Android and iOS builds alike.
Spot a bad rollout early and fix it before it reaches the rest of your users.
## Bug Reports
Let users flag problems the moment they hit them with [Bug Reports](/product/bug-reports), triggered by a device shake or a call to the SDK from your own button. Each report bundles device details, app version, network conditions and screenshots with the user's own words, with an easy link straight to the matching session replay.
Skip the email threads and support ticket back-and-forth. Your users describe the issue and you get all the context you need to solve it.
## User Journeys
See the paths users take through your app with [User Journeys](/product/user-journeys). Every screen transition is mapped automatically into clear flow diagrams, and the exception view shows where issues interrupt those flows.
Short on time? See which paths matter most to your users so you can prioritize issues by user traffic.
## Network Monitoring
Watch every request your app makes with [Network Performance](/product/network-performance). Track HTTP status codes over time and drill into your top endpoints, ranked by latency, error rate and request volume, to find the calls slowing your app down.
Catch degraded endpoints early and tune the API calls that matter most to your users.
## Coding Agents
Bring all of Measure's context into the coding agents you already use. The [Measure MCP server](/product/mcp) gives any agent access to your crashes, errors, performance traces and session replays, straight from your IDE, editor or terminal.
Have it dig into a crash, work through user sessions, or wire it into an agentic triage and debugging pipeline. Whether you prefer commercial tools or open source agents and models, Measure fits right into your workflow.
Works great with Claude Code, OpenAI Codex, Google Antigravity, Cursor, OpenCode, Pi, Devin, Kilo Code, Cline, Roo Code and others.
Get started:
---
Source: https://measure.sh/luciq-alternative
---
# Looking for Luciq alternatives?
Luciq (formerly Instabug) originally started with bug reporting but later expanded to become a full mobile observability platform.
Measure is a mobile first, open source Luciq alternative.
## Full session context on every issue
Measure and Luciq both record full session replays and attach logs, network calls, device details and repro steps to the issues you debug, giving you far more than a stack trace.
Measure captures gestures, navigation, network calls, lifecycle events and custom spans into a full [Session Replay](/product/session-replays) on every issue.
The key difference is transparency. With Measure, you can audit what happens to those collected sessions since our entire platform is open source. From the SDK to the backend processing and the storage layer, you can see what Measure does with your data and verify it yourself. No need for blind trust, just read the source.
## Adaptive capture, on your terms
Measure captures full session context by default, and with [Adaptive Capture](/product/adaptive-capture) you can tune what you collect remotely, without shipping an app update.
Luciq does not give you the same remote control to increase capture while you chase a tricky bug and then pull it back to keep cost and noise down.
Turn detail up on a new release, down afterwards, and change it whenever you need to.
## Fully open source
Luciq is proprietary. Its SDK is published on GitHub, but under a license that forbids modifying it (use as is, all rights reserved), and the backend and dashboard are a closed platform you can neither run nor inspect.
Measure is [fully open source](https://github.com/measure-sh/measure). Read it, run it, self-host it, audit the pipeline end to end, and if you think something can be done better, send a pull request.
## Simple, predictable pricing
Luciq charges per daily active user and per seat and requires a sales call to get a quote. App users without much activity end up adding to costs, and every team member who needs access to the dashboard increases costs further.
Measure has a single, transparent [price](/pricing) based on how much data you use. No per-seat fees, no per-user charges, no sales call needed. With [Adaptive Capture](/product/adaptive-capture) you can tune collection to keep costs in check.
## Built for mobile, by mobile devs
Luciq and Measure are both mobile first platforms. Luciq is closed source and proprietary.
Measure is open source and built in the open, with a public roadmap and issue tracker, made for mobile developers to read, participate and contribute. [Crashes & ANRs](/product/crashes-and-anrs), [App Health](/product/app-health), [Performance Traces](/product/performance-traces), [Network Performance](/product/network-performance), [Bug Reports](/product/bug-reports) and [User Journeys](/product/user-journeys) are all shaped by how mobile apps break in production.
Measure is built with the community, incorporating continuous feedback which we strongly believe leads to a better platform for mobile developers.
## Measure vs Luciq
| Capability | Measure | Luciq |
| --- | --- | --- |
| Crash reporting with full session replays | ✓ | ✓ |
| ANR detection with full session replays | ✓ | ✓ |
| Performance traces | ✓ | ✓ |
| Network monitoring | ✓ | ✓ |
| User journeys | ✓ | ✓ |
| In-app bug reports | ✓ | ✓ |
| Session replay on every issue | ✓ | ✓ |
| Dynamic Sampling with Adaptive Capture | ✓ | ✗ |
| Auto-captured context | Gestures, navigation, network, lifecycle | Screen changes, interactions, network, logs |
| Pricing | Simple pricing based on data usage | Per active user + seat, sales call needed |
| Open Source | Apache 2.0 (OSI open source) | Proprietary |
| Self-hostable | ✓ | ✗ |
| Public roadmap & issue tracker | ✓ | ✗ |
| Mobile focus | ✓ | ✓ |
Get started:
---
Source: https://measure.sh/new-relic-alternative
---
# Looking for New Relic alternatives?
New Relic is a comprehensive, all-in-one observability platform spanning APM, infrastructure, logs, browser monitoring, synthetics and mobile.
Measure is a mobile first, open source New Relic alternative.
## Full session context on every issue
New Relic gives you stack traces, breadcrumbs and interaction traces, and optionally Mobile Session Replay with several sampling options.
Measure attaches a full [Session Replay](/product/session-replays) with gestures, navigation, network calls, lifecycle events and custom spans to every crash, ANR and error and you only pay for the data used as a whole.
The key difference is transparency. Measure is open source, the platform that stores your user data is transparent, and you never have to send your data to a proprietary, locked platform.
## Adaptive capture, not fixed sampling
New Relic offers several sampling strategies. Some of these are server controlled but changing other sample rates means shipping an app update with new SDK settings.
Measure captures full session context by default, and with [Adaptive Capture](/product/adaptive-capture) you can tune what you collect remotely, without shipping an app update.
Dial up on new releases or when chasing tricky production issues, dial down whenever you need to.
## Fully open source
New Relic open sources its mobile agents, but the backend and dashboard are proprietary. You can look into the SDK, but you can't see or run the platform that ingests and stores your data.
Measure is [fully open source](https://github.com/measure-sh/measure). Read it, run it, self-host it, audit the pipeline and if you think something can be done better, send a pull request.
## Simple, predictable pricing
New Relic charges on data ingest and user seats. This means adding teammates and ingesting more data both push the bill up.
Measure has a single, transparent [price](/pricing) based on how much data you use. No per-seat fees, no separate product meters. With [Adaptive Capture](/product/adaptive-capture) you can dial collection up or down without rolling out app updates to control your costs even better.
## Built for mobile, by mobile devs
New Relic monitors infrastructure, APM, logs, browser, synthetics, security and more across one expansive platform. Mobile is one surface among many, and the defaults, dashboards, product decisions and roadmap are shaped by the whole platform rather than by the needs of mobile devs alone.
Measure is built only for mobile. [Crashes & ANRs](/product/crashes-and-anrs), [App Health](/product/app-health), [Performance Traces](/product/performance-traces), [Network Performance](/product/network-performance), [Bug Reports](/product/bug-reports) and [User Journeys](/product/user-journeys) are all designed around how mobile apps actually break in production.
Mobile is not a part of our product, it is the whole product.
## Measure vs New Relic
| Capability | Measure | New Relic |
| --- | --- | --- |
| Crash reporting with full session replays | ✓ | Crash reports with optional Session replays |
| ANR detection with full session replays | ✓ | ANRs with optional Session replays |
| Performance traces | ✓ | ✓ |
| Network monitoring | ✓ | ✓ |
| User journeys | ✓ | ✓ |
| In-app bug reports | ✓ | ✗ |
| Session replay on every issue | ✓ | Session replay, sampled |
| Dynamic Sampling with Adaptive Capture | ✓ | Sampled, partial remote control |
| Auto-captured context | Gestures, navigation, network, lifecycle | Interactions, network, handled exceptions, breadcrumbs |
| Pricing | Simple pricing based on data usage | Per-GB data ingest plus per-user seats |
| Open Source | Apache 2.0 (OSI open source) | SDKs only |
| Self-hostable | ✓ | ✗ |
| Public roadmap & issue tracker | ✓ | SDK repos only |
| Mobile focus | ✓ | One small part of a huge platform |
Get started:
---
Source: https://measure.sh/pricing
---
# Pricing
Simple pricing based on the data used. No stressing over seat limits. No need to buy artificial bundles of crashes and spans - just track what you need to get to the root cause faster.
## Free — $0 per month
- 5 GB per month
- 30 days retention
- No credit card needed
## Pro — $50 per month
- 25 GB per month included
- 90 days retention
- Extra data charged at $2.00 per GB/month
Control costs with [Adaptive Capture](/product/adaptive-capture) · No Seat Limits · No Artificial Bundles
Get started:
---
Source: https://measure.sh/product/adaptive-capture
---
# Adaptive Capture
Most monitoring data is never read but ends up inflating your costs 💰. Adaptive Capture lets you capture what matters based on changing needs.
Need more data during a product launch or incident? Simply tweak your collection parameters to capture additional context when it matters most and collect only the essentials when things are running smoothly.
The best part? No need to roll out app updates! When you change your captures settings, our servers propagate the changes to our SDK seamlessly.
No more worrying about bloated costs or wasted data, Adaptive Capture lets you get the data you need, when you need it.
Get started:
---
Source: https://measure.sh/product/agent
---
# Measure Agent
Debug your apps with full context about crashes, errors, sessions and traces from Slack or your coding agent.
Measure Agent turns a question like "how are crashes looking today?" or "which endpoints got slower after the last release?" into the right query, then replies with concrete numbers and the sessions, errors and traces behind them. No dashboards to build and no query syntax to learn.
Use it where you already work: right inside Slack, or from your coding agent over MCP.
## Debug from your coding agent
Measure Agent is also available from Measure's MCP server, so you can start debugging straight from your editor or terminal. Your coding agent can query it to fix a crash, walk a session, or run an agentic triage loop.
Works great with Claude Code, OpenAI Codex, Google Antigravity, Cursor, OpenCode, Pi, Devin, Kilo Code, Cline, Roo Code and others.
Get started:
---
Source: https://measure.sh/product/app-health
---
# App Health
Keep your finger on the pulse of your app's performance with comprehensive health monitoring that goes beyond the basics.
App Health gives you fast insights into the metrics that matter most - from error rates, error rates as perceived by users, app adoption and app size to precise launch time measurements across cold, warm and hot starts.
With App Health, you can proactively identify and address performance issues before they impact your users leading to a smooth rollout every time.
Get started:
---
Source: https://measure.sh/product/bug-reports
---
# Bug Reports
Empower your users to report issues directly from your app with a device shake or using your own custom button.
Bug Reports automatically capture everything that matters - device information, app version, network conditions and the exact timestamp alongside the user's description and screenshots.
Every bug report links directly to the complete session replay, so you can see exactly what the user experienced, review the sequence of events and identify the root cause without stumbling around in the dark.
Bug Reports allows you to skip the email threads, support tickets and the back-and-forth asking users to remember what they were doing - your users describe the problem in their own words and you get all the technical data you need to solve it.
Get started:
---
Source: https://measure.sh/product/crashes-and-anrs
---
# Crashes and ANRs
Get instant visibility into every exception with detailed crash reports that include full stack traces, device information, OS versions and intelligent analysis of the sequence of user actions that led to the failure.
Our Common Path feature reconstructs the user journey before each crash, showing you what screens they visited, which actions they took, what API calls were and several other important signals.
Path analysis combined with comprehensive stack traces and thread-level details, gives you everything you need to reproduce issues effectively and ship fixes with confidence.
Get started:
---
Source: https://measure.sh/product/mcp
---
# MCP Server
Connect Measure with your favorite coding agents through the Model Context Protocol.
MCP lets AI coding agent access your errors, performance traces, session replays and bug reports directly in your development workflow.
With MCP, you can simply ask your AI assistant to look up an error, let it fetch stack traces, session replays and related context, and ship fixes faster than ever before.
## Works with your favorite coding agents
Built on an open standard, Measure plugs into whatever coding agent you already use. Connect your agent to the Measure MCP server and it can pull your app telemetry, then help you debug an issue, walk a user session, or run an agentic triage and debugging pipeline.
Works great with Claude Code, OpenAI Codex, Google Antigravity, Cursor, OpenCode, Pi, Devin, Kilo Code, Cline, Roo Code and others.
Get started:
---
Source: https://measure.sh/product/network-performance
---
# Network Performance
Monitor the health and performance of every network request your app makes. Instantly see HTTP status code distributions over time, giving you a clear picture of how your API layer is performing at a glance.
Drill into your top endpoints ranked by latency, error rate and request frequency to pinpoint exactly which calls are slowing down your app or failing silently. Visualize when specific endpoints are called during a session to understand request patterns and timing.
With Network Performance, you can proactively catch degraded endpoints, reduce error rates and optimize the API calls that matter most to your users.
Get started:
---
Source: https://measure.sh/product/performance-traces
---
# Performance Traces
Measure exactly what matters for your app's user experience by instrumenting critical operations in your codebase.
Performance traces let you understand how API fetches, complex code operations and UI rendering stack up within a single user flow or aggregate across millions of sessions, with waterfall charts that make bottlenecks immediately obvious.
Every trace includes rich context such as device type and network conditions and links to a full session replay so you can spot patterns and correlate slowdowns within specific environments.
Whether you're reducing checkout time, speeding up content loading or improving screen transitions, Performance Traces give you the quantitative data you need to make precise improvements.
Get started:
---
Source: https://measure.sh/product/session-replays
---
# Session Replays
Debug issues faster by replaying the exact sequence of events that led to a crash or performance problem.
Session Replay captures the complete story - see which API call failed, what the user clicked right before an error occurred and how your app's resources were behaving at that precise moment.
With Session Replays, you can stop guessing and have the full context you need to identify and fix root causes in an easy-to-navigate replay.
Get started:
---
Source: https://measure.sh/product/user-journeys
---
# User Journeys
See the full picture of user behavior with beautiful flow diagrams that reveal the actual paths users take through your app.
User Journeys automatically map every screen transition, showing you which flows are most popular, where users drop off and which navigation patterns you never anticipated. Easily add your own screens and views to enrich them further.
Toggle between normal path analysis and exception view to see exactly where crashes and ANRs interrupt user flows. If users consistently crash when navigating from Product List to Product Detail screens, you'll see it highlighted with crash counts and session volumes. Click any path or exception to drill into the details and investigate further.
Whether you're redesigning navigation, prioritizing feature work or debugging issues in conversion funnels, User Journeys transforms complex behavioral data into clear, actionable visualizations that help you build better experiences.
Get started:
---
Source: https://measure.sh/security
---
# Security
At Measure, we recognize that security is fundamental to the trust placed in our open-source software and Measure Cloud (our managed SaaS offering).
Security is a core priority throughout the development, deployment and maintenance of our systems, and we adhere to established industry best practices to safeguard code, data and operations.
This policy outlines the principles, standards and controls that guide our commitment to maintaining the confidentiality, integrity and availability of our software, infrastructure and services.
## Open Source Transparency
Measure is fully open-source, and its source code is publicly available on [GitHub](https://github.com/measure-sh/measure). This transparency allows continuous review by the open-source community, fostering early identification and remediation of potential security issues.
## Supply Chain Security
Dependencies are regularly audited through GitHub [Dependabot](https://github.com/measure-sh/measure/security/dependabot) and code scanning tools, including [secret scanning](https://github.com/measure-sh/measure/security/secret-scanning), to detect and address vulnerabilities promptly. We ensure timely updates to dependencies to mitigate risks. An up-to-date [Software Bill of Materials](https://github.com/measure-sh/measure/network/dependencies) (SBOM) is maintained, and users are encouraged to inspect or export it for their own assessments.
## Authentication & Authorization
**Open Source (Self Hosted)**: Authentication and authorization are provided via Google and GitHub OAuth 2.0 protocols, strictly following the latest version of the [OAuth](https://oauth.net/specs/) standards. We use JSON Web Tokens (JWT) with a short expiry to minimize potential exploitation. We do not ask for or store user passwords, eliminating password-based vulnerabilities. Database credentials for Postgres and ClickHouse are generated using cryptographically secure algorithms via OpenSSL, unique to each installation. All communications with the software should be conducted over secure channels (TLS 1.2 or higher) when exposing APIs externally.
**Measure Cloud**: Authentication is managed centrally, with the same OAuth 2.0 and JWT standards applied. Infrastructure and secret management follow cloud security best practices, including encryption at rest and in transit. All Measure APIs use TLS 1.2 or higher for encryption in transit to protect data integrity and confidentiality. Private keys and sensitive credentials are securely stored and managed using [Google Secret Manager](https://cloud.google.com/security/products/secret-manager). Role-based access controls and centralized key management are enforced.
## Data Security
**Open Source (Self Hosted)**: Measure does not process or store sensitive customer data by design. All data is stored within the user's infrastructure. Users are responsible for performing security assessments and implementing safeguards appropriate to their environment.
**Measure Cloud**: Customer data is processed and stored within Measure Cloud infrastructure, hosted on Google Cloud Platform. All Google Cloud Storage (GCS) buckets are encrypted at rest using Google-managed encryption keys with the **AES-256** algorithm. Data in transit, including API requests and responses, is encrypted using **TLS 1.2** or higher. We apply strict access controls, monitoring and regular security reviews to ensure data security. Our architecture follows the principle of least privilege and enforces separation between customer environments. Measure Cloud adheres to Google's security best practices, as outlined in the [Google Cloud Security Best Practices Center](https://cloud.google.com/security/best-practices).
## Data Retention
**Session Data**: All session-related data is retained only according to the user-configured application data retention settings in the Measure Dashboard. Users have full control over retention periods for their applications.
**Crash and ANR Metadata**: We retain minimal crash and ANR (Application Not Responding) metadata solely to facilitate the identification and resolution of recurring issues in future application sessions. This data is anonymized and does not contain personally identifiable information (PII) by design.
## Monitoring for Performance & Reliability
We continuously monitor the health, availability and performance of our software using industry-standard observability tools to ensure operational reliability. Monitoring is designed to detect and address potential security issues proactively. Collected data does not include customer application data or personally identifiable information (PII) by design and is used solely to maintain performance, stability and security.
## Vulnerability Reporting
We encourage responsible disclosure of security vulnerabilities via [GitHub's Security Advisory](https://github.com/measure-sh/measure?tab=security-ov-file) process. Potential security issues may also be reported by emailing for prompt investigation and remediation.
Get started:
---
Source: https://measure.sh/sentry-alternative
---
# Looking for Sentry alternatives?
Sentry is a popular error monitoring tool with roots in the web dev world. Mobile support is a more recent expansion to the core error monitoring platform.
Measure is a mobile first, open source Sentry alternative.
## Full session context on every issue
Sentry gives you stack traces and breadcrumbs out of the box with Session Replays billed separately. If you want full context on every error, you will need to turn on Session Replays for all of them and accommodate the considerable cost increase.
Measure attaches a full [Session Replay](/product/session-replays) with gestures, navigation, network calls, lifecycle events and custom spans to every crash, ANR and error and you only pay for the data used as a whole.
You see exactly what the user did and what the app did, on every issue, without any compromise on the context.
## Adaptive capture, not fixed sampling
Sentry uses client-side sampling. You set a sample rate for traces and replays and decide up front what fraction to keep.
Measure captures full session context by default, and with [Adaptive Capture](/product/adaptive-capture) you can tune what you collect remotely, without shipping an app update.
Dial up sample rates on new releases or when chasing tricky production issues, dial down whenever you need to.
## Fully open source
Sentry uses a custom source-available rather than OSI open source: its main application and dashboard ship under the Functional Source License (FSL) with only the SDKs being MIT and the main application only going Apache 2.0 after 2 years.
Measure is [fully open source](https://github.com/measure-sh/measure). Read it, run it, self-host it, audit the pipeline and if you think something can be done better, send a pull request.
## Simple, predictable pricing
Sentry bills across a range of separate features: errors, spans, replays all cost different amounts across various tiers which makes it harder to predict costs as your app scales up.
Measure has a single, transparent [price](/pricing) based on how much data you use. No per-seat fees, no separate product meters. With [Adaptive Capture](/product/adaptive-capture) you can dial collection up or down without rolling out app updates to control your costs even better.
## Built for mobile, by mobile devs
Sentry monitors servers, cloud, serverless, frontend, games and mobile across dozens of SDKs. Mobile is one platform among many, and the defaults, platform decisions, dashboards and product roadmap are shaped by the whole platform rather than by the needs of mobile devs alone.
Measure is built only for mobile. [Crashes & ANRs](/product/crashes-and-anrs), [App Health](/product/app-health), [Performance Traces](/product/performance-traces), [Network Performance](/product/network-performance), [Bug Reports](/product/bug-reports) and [User Journeys](/product/user-journeys) are all designed around how mobile apps actually break in production.
Mobile is not a part of our product, it is the whole product.
## Measure vs Sentry
| Capability | Measure | Sentry |
| --- | --- | --- |
| Crash reporting with full session replays | ✓ | Crash reports with breadcrumbs, Session replay billed separately |
| ANR detection with full session replays | ✓ | ANRs with breadcrumbs, Session replay billed separately |
| Performance traces | ✓ | ✓ |
| Network monitoring | ✓ | ✓ |
| User journeys | ✓ | ✗ |
| In-app bug reports | ✓ | ✓ |
| Session replay on every issue | ✓ | Session Replay billed separately |
| Dynamic Sampling with Adaptive Capture | ✓ | Client side only sampling |
| Auto-captured context | Gestures, navigation, network, lifecycle | Breadcrumbs, deeper context via Replay |
| Pricing | Simple pricing based on data usage | Separate quotas for errors, spans, replays, profiling, cron, uptime & logs |
| Open Source | Apache 2.0 (OSI open source) | FSL — source-available, Apache 2.0 after 2 years |
| Self-hostable | ✓ | ✓ |
| Public roadmap & issue tracker | ✓ | ✓ |
| Mobile focus | ✓ | One of many Sentry platforms |
Get started:
---
Source: https://measure.sh/why-measure
---
# Why Measure?
There are several mobile app monitoring tools in the market. What is different between these tools is the core philosophy of the teams building them which affects the products in small and large ways.
## Beyond Crashes
Measure is focused on giving the full picture of apps in production to mobile developers and is not just limited to basic error tracking.
With advanced automated capture techniques and full [Session Replays](/product/session-replays), Measure allows you to get the full context of errors and performance issues as they happen in the wild.
If you're looking to truly understand what issues occur in your app, how they impact users and how to debug them quickly, Measure is the right tool for you.
## Mobile Focus
Measure is built by mobile developers for mobile developers. Every feature, every design decision and every trade-off is made with mobile app production monitoring in mind.
Mobile is not an add-on or afterthought to an observability product, it **is** the product.
If you care about your app being treated with the respect and focus it deserves, Measure is the perfect match for you.
## Open Source
Measure is [fully open source](https://github.com/measure-sh/measure). This means our code is open to scrutiny and community contributions which we strongly believe leads to a better product.
If you value transparency, flexibility and open development, Measure is the right community for you.
## Simple Pricing
Measure offers clear and transparent [pricing](/pricing) based on data and retention. There are no bundles, hidden charges, seats or tiers (although we do offer discounts for high volume apps). You can send any combination of errors, metrics, spans etc without worrying about exceeding artificial limits.
Further, with [Adaptive Capture](/product/adaptive-capture), you can optimize your data collection to only capture what you need and adjust it dynamically without rolling out app updates, reducing costs and data bloat.
If you hate playing with excel sheets and pricing calculators to figure out your monthly bill and would like to adjust your data collection based on changing needs, Measure is the right choice for you.
Get started: