--- Source: https://measure.sh/docs --- # Documentation ### Table of Contents * [**Integrate the SDK**](#integrate-the-sdk) — Set up the measure-sh SDK in your mobile app * [**Explore Features**](#explore-features) — Discover all available features * [**Configuration Options**](#configuration-options) — Customize SDK behavior * [**Performance Impact**](#performance-impact) — Assess the SDK's impact on app performance **Additional Info** * [**SDK Upgrade Guides**](https://measure.sh/docs/sdk-upgrade-guides) — Upgrade to the latest SDK versions for Android and iOS * [**Self-Hosting Guide**](https://measure.sh/docs/hosting) - Host measure-sh on your own infrastructure # Integrate the SDK Check out the [SDK Integration Guide](https://measure.sh/docs/sdk-integration-guide) to learn how to set up measure-sh for your app. Then refer to the documentation below for details on features and how to make the best use of measure-sh for your app. # Explore Features Explore the following pages which include instructions and configuration options to help you understand how to leverage different features in your mobile applications. Also, review the 'How It Works' section in each feature's documentation to understand its underlying mechanism and enhance your ability to use it effectively. * [**Session Timelines**](https://measure.sh/docs/features/feature-session-timelines) — Find and view session timelines * [**Crash Reporting**](https://measure.sh/docs/features/feature-crash-reporting) — Analyze app crashes * [**ANR Reporting**](https://measure.sh/docs/features/feature-anr-reporting) — Analyze Application Not Responding (ANR) issues * [**Error Tracking**](https://measure.sh/docs/features/feature-error-tracking) — Track and analyze handled errors in your app * [**Gesture Tracking**](https://measure.sh/docs/features/feature-gesture-tracking) — Automatically track user gestures in your app * [**Performance Tracing**](https://measure.sh/docs/features/feature-performance-tracing) — Monitor app performance with traces * [**Custom Events**](https://measure.sh/docs/features/feature-custom-events) — Capture custom events in your app * **Bug Reporting** — Let users report bugs directly from your app * [**Android**](https://measure.sh/docs/features/feature-bug-report-android) * [**iOS**](https://measure.sh/docs/features/feature-bug-report-ios) * [**Flutter**](https://measure.sh/docs/features/feature-bug-report-flutter) * [**Screenshot Masking for SwiftUI**](https://measure.sh/docs/features/feature-screenshot-masking-swiftui) — Mask sensitive content in SwiftUI views when capturing screenshots * [**Screenshot Masking for Flutter**](https://measure.sh/docs/features/feature-screenshot-masking-flutter) — Mask sensitive content in Flutter widgets when capturing screenshots * [**App Launch Metrics**](https://measure.sh/docs/features/feature-app-launch-metrics) — Measure app launch performance * [**Network Monitoring**](https://measure.sh/docs/features/feature-network-monitoring) — Monitor HTTP requests and responses * [**Network Connectivity Changes**](https://measure.sh/docs/features/feature-network-connectivity-changes) — Track when network connectivity changes * [**Navigation & Lifecycle Tracking**](https://measure.sh/docs/features/feature-navigation-lifecycle-tracking) — Track app navigation and lifecycle events * [**CPU Monitoring**](https://measure.sh/docs/features/feature-cpu-monitoring) — Monitor CPU usage for every session * [**Memory Monitoring**](https://measure.sh/docs/features/feature-memory-monitoring) — Monitor memory usage for every session * [**Identify Users**](https://measure.sh/docs/features/feature-identify-users) — Correlate sessions with a user ID * [**Manually Start or Stop the SDK**](https://measure.sh/docs/features/feature-manually-start-stop-sdk) — Control when data collection happens * [**App Size Monitoring**](https://measure.sh/docs/features/feature-app-size-monitoring) — Monitor app size changes * [**Alert Notifications**](https://measure.sh/docs/features/feature-alerts) — Receive Crash & ANR spike alerts and Daily Summaries for core app metrics. * [**Slack Integration**](https://measure.sh/docs/features/feature-slack-integration) — Connect Slack to receive alert notifications and daily summaries * [**MCP Server**](https://measure.sh/docs/features/feature-mcp) — Let AI tools like Claude Code query your app's crash and error data # Configuration Options Measure provides a number of configuration options to customize data collection and SDK behavior. These options are available in two ways: * **SDK Options** — Set at initialization time in your app's code. * **Remote Configuration Options** — Configured remotely from the Measure dashboard. Changes take effect without releasing a new app version. Read more about [Configuration Options](https://measure.sh/docs/features/configuration-options). # Performance Impact Read the [Performance Impact](https://measure.sh/docs/features/performance-impact) documentation to understand how the SDK affects your app's performance. --- Source: https://measure.sh/docs/sdk-integration-guide --- # Getting Started - [1. Create an App](#1-create-an-app) - [2. Set Up the SDK](#2-set-up-the-sdk) - [Android](#android) - [iOS](#ios) - [Flutter](#flutter) - [React Native](#react-native) - [Kotlin Multiplatform](#kotlin-multiplatform) - [3. Verify Installation](#3-verify-installation) - [4. Review Configuration Options](#4-review-configuration-options) - [Troubleshoot](#troubleshoot) ## 1. Create an App Create a new app by visiting the _Apps_ section on the dashboard. Once the app is created, note the `API URL` & `API Key` for your app. This will be used in the SDK configuration in later steps. ![Create new app](https://measure.sh/docs/assets/create-app.png) ## 2. Set Up the SDK - [Android](#android) - [iOS](#ios) - [Flutter](#flutter) - [React Native](#react-native) - [Kotlin Multiplatform](#kotlin-multiplatform) ## Android
Minimum Requirements | Name | Version | | --------------------- | --------------- | | Android Gradle Plugin | `8.1.0` | | Min SDK | `21` (Lollipop) | | Target SDK | `35` |
Self-host Compatibility | 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` |
### Add the API Key & API URL Add the API URL & API Key to your application's `AndroidManifest.xml` file. ```xml ```
Configure API Keys for Different Build Types You can use [manifestPlaceholders](https://developer.android.com/build/manage-manifests#inject_build_variables_into_the_manifest) to configure different values for different build types or flavors. In the `build.gradle.kts` file: ```kotlin android { buildTypes { debug { manifestPlaceholders["measureApiKey"] = "YOUR_API_KEY" manifestPlaceholders["measureApiUrl"] = "YOUR_API_URL" } release { manifestPlaceholders["measureApiKey"] = "YOUR_API_KEY" manifestPlaceholders["measureApiUrl"] = "YOUR_API_URL" } } } ``` or in the `build.gradle` file: ```groovy android { buildTypes { debug { manifestPlaceholders = ["measureApiKey": "YOUR_API_KEY"] manifestPlaceholders = ["measureApiUrl": "YOUR_API_URL"] } release { manifestPlaceholders = ["measureApiKey": "YOUR_API_KEY"] manifestPlaceholders = ["measureApiUrl": "YOUR_API_URL"] } } } ``` Then add the following in the `AndroidManifest.xml` file: ```xml ```
### Add the Gradle Plugin Add the following plugin to your project. ```kotlin plugins { id("sh.measure.android.gradle") version "0.13.0" } ``` or, use the following if you're using `build.gradle`. ```groovy plugins { id 'sh.measure.android.gradle' version '0.13.0' } ```
Configure Variants By default, the plugin is applied to all variants. To disable the plugin for specific variants, use the `measure` block in your build file. > [!IMPORTANT] > Setting `enabled` to `false` will disable the plugin for that variant. This prevents the plugin from > collecting `mapping.txt` file and other build information about the app. Features like tracking app size, > de-obfuscating > stack traces, etc. will not work. For example, to disable the plugin for `debug` variants, add the following to your `build.gradle.kts` file: ```kotlin measure { variantFilter { if (name.contains("debug")) { enabled = false } } } ``` or in the `build.gradle` file: ```groovy measure { variantFilter { if (name.contains("debug")) { enabled = false } } } ```
### Add the SDK Add the following to your app's `build.gradle.kts` file. ```kotlin implementation("sh.measure:measure-android:0.18.0") ``` or, add the following to your app's `build.gradle` file. ```groovy implementation 'sh.measure:measure-android:0.18.0' ``` ### Initialize the SDK Add the following to your app's Application class `onCreate` method. > [!IMPORTANT] > To be able to detect early crashes and accurate launch time metrics, initialize the SDK as soon as possible in > Application `onCreate` method. ```kotlin Measure.init( context, MeasureConfig( // Enable full collection in debug mode // to verify installation enableFullCollectionMode = true, ) ) ``` ### Enable Full Collection Mode The init snippet above sets `enableFullCollectionMode = true`, which forces all data to be sent to the server regardless of sampling. This makes it easy to verify the installation by confirming that events from your app are reaching the dashboard. > [!IMPORTANT] > Enabling full collection mode in production can lead to high costs. Disable or remove this flag for release > builds and rely on sampling instead. See [Configuration Options](https://measure.sh/docs/features/configuration-options) for details. See the [troubleshooting](#troubleshoot) section if you face any issues. ## iOS
Minimum Requirements | Name | Version | | ----------------------- | ------- | | Xcode | 15.0+ | | Minimum iOS Deployments | 12.0+ | | Swift Version | 5.10+ |
Self-host Compatibility | SDK Version | Minimum Required Self-host Version | | ----------- | ---------------------------------- | | >=0.1.0 | 0.6.0 | | >=0.7.0 | 0.9.0 |
### Install the SDK Measure SDK supports **CocoaPods** and **Swift Package Manager (SPM)** for installation. #### Using CocoaPods [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate MeasureSDK into your Xcode project using CocoaPods, specify it in your `Podfile`: ```ruby pod 'measure-sh' ``` > [!NOTE] > MeasureSDK must be linked statically. If you are using `use_frameworks!` in your Podfile, you will need to ensure `measure-sh` is linked statically, as dynamic linking is not supported. CocoaPods does not natively support per-pod linkage overrides. You will need to install the [`cocoapods-pod-linkage`](https://github.com/microsoft/cocoapods-pod-linkage) plugin: ```sh gem install cocoapods-pod-linkage ``` Then add the plugin and linkage option to your `Podfile`: ```ruby plugin 'cocoapods-pod-linkage' target 'YourApp' do use_frameworks! pod 'measure-sh', :linkage => :static # ... rest of your pods end ``` Alternatively, if all your pods can be linked statically, you can use: ```ruby use_frameworks! :linkage => :static ``` #### Using Swift Package Manager The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. Add Measure as a dependency by adding `dependencies` value to your `Package.swift` or the Package list in Xcode. ```swift dependencies: [ .package(url: "https://github.com/measure-sh/measure.git", branch: "ios-v0.11.0") ] ``` ### Initialize the SDK Add the following to your AppDelegate's `application(_:didFinishLaunchingWithOptions:)` to capture early crashes and launch time metrics. > [!IMPORTANT] > To detect early crashes and ensure accurate launch time metrics, initialize the SDK as soon as possible > in `application(_:didFinishLaunchingWithOptions:)`. ```swift import Measure func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let config = BaseMeasureConfig( // Set to true to track all sessions // useful to verify the installation enableFullCollectionMode: true ) let clientInfo = ClientInfo(apiKey: "", apiUrl: "") Measure.initialize(with: clientInfo, config: config) return true } ``` ```objc #import - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ClientInfo *clientInfo = [[ClientInfo alloc] initWithApiKey:@"" apiUrl:@""]; BaseMeasureConfig *config = [[BaseMeasureConfig alloc] initWithEnableLogging:YES autoStart:YES requestHeadersProvider:NULL maxDiskUsageInMb:50 enableFullCollectionMode:YES enableDiagnosticMode:NO enableDiagnosticModeGesture:NO]; [Measure initializeWith:clientInfo config:config]; return YES; } ``` ### Enable Full Collection Mode The init snippet above sets `enableFullCollectionMode` to `true`, which forces all data to be sent to the server regardless of sampling. This makes it easy to verify the installation by confirming that events from your app are reaching the dashboard. > [!IMPORTANT] > Enabling full collection mode in production can lead to high costs. Disable or remove this flag for release > builds and rely on sampling instead. See [Configuration Options](https://measure.sh/docs/features/configuration-options) for details. ## Flutter The Flutter SDK currently supports only Android and iOS targets and is not available for web or desktop. The SDK depends on the native Android and iOS SDKs, so all the minimum requirements for Android and iOS apply to the Flutter SDK as well.
Minimum Requirements | Name | Version | | ------- | ------- | | Flutter | `3.24` |
Self-host Compatibility | SDK Version | Minimum Required Self-host Version | | ----------- | ---------------------------------- | | >= `0.4.0` | `0.10.0` (releasing soon) | | >= `0.3.0` | `0.9.0` | | >= `0.1.0` | `0.8.0` |
### Install the SDK Add the following dependency to your `pubspec.yaml` file: ```yaml dependencies: measure_flutter: ^0.6.0 ``` ### Initialize the SDK To initialize the SDK, you need to call the `Measure.instance.init` method in your `main` function. - Run app inside the callback passed to the `init` method. This ensures that the Measure SDK can set up error handlers to track uncaught exceptions. - Wrap your app with the `MeasureWidget`, this is required for gesture tracking and screenshots. > [!IMPORTANT] > To detect early native crashes and to ensure accurate launch time metrics, initialize the Android SDK in > `Application` class as described in the [Android](#initialize-the-sdk) section and the iOS SDK in `AppDelegate` as > described in > the [iOS](#initialize-the-sdk-1) section. It is highly recommended to initialize both native SDKs even when using the > Flutter SDK. ```dart Future main() async { await Measure.instance.init( () => runApp( // wrap your app with MeasureWidget MeasureWidget(child: MyApp()), ), // SDK configuration config: const MeasureConfig( enableLogging: true, ), ); } ``` This does the following: - Initializes the Measure SDK with the provided `config`. - Wraps your app with the `MeasureWidget` for gesture detection and layout snapshots. - Sets up the error handlers to track uncaught exceptions. ### Enable Full Collection Mode The Flutter SDK depends on the native SDKs, so `enableFullCollectionMode` must be set to `true` in both the Android and iOS native SDK initializations (see the Android and iOS sections above). This forces all data to be sent to the server regardless of sampling, which makes it easy to verify the installation by confirming that events from your app are reaching the dashboard. > [!IMPORTANT] > Enabling full collection mode in production can lead to high costs. Disable or remove this flag for release > builds and rely on sampling instead. See [Configuration Options](https://measure.sh/docs/features/configuration-options) for details. ### Flutter Android Setup Measure Flutter SDK depends on the native Android SDK, so you need to follow all the steps mentioned in the [Android](#android) section to set up the Android SDK properly. 1. [Add API Key & API URL to Android Manifest](#add-the-api-key--api-url) 2. [Add Android Gradle Plugin](#add-the-gradle-plugin) 3. [Initialize the SDK](#initialize-the-sdk) ### Flutter iOS Setup Measure Flutter SDK depends on the native iOS SDK, so you need to follow all the steps mentioned in the [iOS](#ios) section to set up the iOS SDK properly. 1. [Install the SDK using CocoaPods or SPM](#install-the-sdk) 2. [Initialize the SDK](#initialize-the-sdk-1) ### Track navigation See [Navigation Monitoring](https://measure.sh/docs/features/feature-navigation-lifecycle-tracking) for instructions on how to track navigation events. ### Track http requests See [Network Monitoring](https://measure.sh/docs/features/feature-network-monitoring) for instructions on how to track HTTP requests. ### Gesture tracking & Layout Snapshots The Flutter SDK automatically captures gestures like clicks, long clicks and scrolls. It also captures layout snapshots on every click to help visualize user interactions. To enable these features, simply wrap your app with the `MeasureWidget` as shown in the initialization step above. Read more about adding custom widget names in the layout snapshots in [Gesture Tracking & Layout Snapshots](https://measure.sh/docs/features/feature-gesture-tracking). ## React Native The React Native SDK supports both **Expo** and **Vanilla React Native** projects on Android and iOS.
Minimum Requirements | Name | Version | | ------------ | -------- | | React Native | `0.72.0` | | React | `18.2.0` |
> [!IMPORTANT] > Symbolication support for Over-The-Air (OTA) releases is coming soon. ### Install the SDK ```sh npm install @measuresh/react-native@0.1.1 ``` or with yarn: ```sh yarn add @measuresh/react-native@0.1.1 ``` --- ### Expo The recommended setup for Expo projects uses the Measure config plugin, which automates the native configuration for both Android and iOS. #### 1. Add the plugin to `app.json` ```json { "expo": { "plugins": [ [ "@measuresh/react-native", { "androidApiKey": "", "androidApiUrl": "", "iosApiKey": "", "iosApiUrl": "" } ] ] } } ``` The plugin automatically handles: **Android** - Injects `sh.measure.android.API_KEY` and `sh.measure.android.API_URL` into `AndroidManifest.xml` - Adds the Measure Gradle plugin to the project build files - Adds the `measure-android` dependency to `app/build.gradle` **iOS** - Adds the `MeasureReactNative` pod to `Podfile` - Adds `export SOURCEMAP_FILE="$(pwd)/main.jsbundle.map"` to the "Bundle React Native code and images" build phase so a sourcemap is generated on every Release build - Adds an "Upload Measure Symbol Files" build phase that automatically uploads dSYM files and the JavaScript sourcemap after each Release build #### 2. Run prebuild ```sh npx expo prebuild ``` #### 3. Initialize the SDK Call `Measure.init` as early as possible in your app entry point: ```typescript import { Measure, MeasureConfig } from "@measuresh/react-native"; import { useEffect } from "react"; export default function App() { useEffect(() => { Measure.init({ config: new MeasureConfig({ autoStart: true }), }); }, []); // ... } ``` #### 4. Build and run ```sh # Android npx expo run:android # iOS npx expo run:ios ``` --- ### Vanilla React Native #### Android setup Step 1 — Add API credentials to `AndroidManifest.xml` ```xml ``` **Step 2 — Add the Gradle plugin** In your project-level `build.gradle`: ```groovy buildscript { dependencies { classpath("sh.measure.android.gradle:sh.measure.android.gradle.gradle.plugin:0.13.0") } } ``` In your app-level `build.gradle` (after all other plugins): ```groovy apply plugin: "sh.measure.android.gradle" ``` The Gradle plugin automatically uploads ProGuard/R8 mapping files and JavaScript sourcemaps after every `assembleRelease` or `bundleRelease` build — no manual upload step is needed. #### iOS setup **Step 1 — Add the pod** In your `Podfile`: ```ruby pod 'MeasureReactNative', :path => '../node_modules/@measuresh/react-native' ``` Then run: ```sh pod install ``` **Step 2 — 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 3 — Add the upload build phase** Add a new Run Script build phase **after** the bundle phase: ```sh "$SRCROOT/../node_modules/@measuresh/react-native/scripts/upload_build_phase.sh" \ "" \ "" ``` This script automatically uploads dSYM files and the JavaScript sourcemap after each Release build. See the caution note below about when to run it. > [!CAUTION] > The upload script runs on every build in the configuration you add it to. To restrict it to Archive builds only, wrap the script content in: > > ```sh > if [ "$ACTION" = "archive" ]; then > # script content here > fi > ``` #### Initialize the SDK Call `Measure.init` as early as possible in your app entry point: ```typescript import { Measure, MeasureConfig } from "@measuresh/react-native"; import { useEffect } from "react"; export default function App() { useEffect(() => { Measure.init({ config: new MeasureConfig({ autoStart: true }), }); }, []); // ... } ``` #### Build and run ```sh # Android npx react-native run-android # iOS npx react-native run-ios ``` --- ### Track navigation See [Navigation Monitoring](https://measure.sh/docs/features/feature-navigation-lifecycle-tracking) for instructions on how to track navigation events. ### Track http requests See [Network Monitoring](https://measure.sh/docs/features/feature-network-monitoring) for instructions on how to track HTTP requests. ## Kotlin Multiplatform The KMP SDK provides access to Measure API from shared Kotlin code (`commonMain`) on Android and iOS. It is a thin wrapper over the native Android and iOS SDKs, so all the minimum requirements for Android and iOS apply to the KMP SDK as well. ### Minimum Requirements | Name | Version | | --------------------- | ------------------- | | Kotlin | `2.x` | | Measure Android SDK | `0.18.0` | | Measure iOS SDK | `0.11.0` | The SDK is built with Kotlin `2.3.20`. Use a compatible Kotlin `2.x` toolchain in your project. ### Add the Native SDKs The KMP SDK does not have its own initialization API. You initialize each native SDK in its own platform target (as described in the [Android](#android) and [iOS](#ios) sections), and then use `sh.measure.kmp.Measure` from shared code. ### Add KMP SDK Add the dependency to the `commonMain` source set of your shared module's `build.gradle.kts` file: ```kotlin kotlin { sourceSets { commonMain.dependencies { implementation("sh.measure:measure-kmp:0.1.0") } } } ``` ### Use the SDK from shared code Once both native SDKs are initialized, you can call any API from `sh.measure.kmp.Measure` in `commonMain`: ```kotlin 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")), ) ``` ### Crashes from shared Kotlin code on iOS Crashes from unhandled exceptions in shared Kotlin code are captured automatically on iOS, no extra setup is required. The SDK installs a Kotlin exception hook on load and forwards the crash, with its Kotlin stack frames preserved. ## 3. Verify Installation Launch the app with the SDK integrated and navigate through a few screens. The data is sent to the server periodically, so it may take a few seconds to appear. Checkout the `Usage` section in the dashboard or navigate to the `Session Timelines` tab to see the data. 🎉 Congratulations! You have successfully integrated Measure into your app! --- ## 4. Review Configuration Options There are several configuration options available to customize the SDK behavior. Some options can be set during SDK initialization, while others can be configured remotely from the dashboard. Review the [Configuration Options](https://measure.sh/docs/features/configuration-options) section to learn more about these options and how to use them effectively. For debug builds, it's recommended to set `enableFullCollectionMode` to `true` during initialization to ensure all data is collected for verification purposes. In release builds, you can adjust the sampling rates and other settings as needed to balance signal vs noise and optimize costs. ## Troubleshoot ### Enable full collection mode for debug builds Set `enableFullCollectionMode` to `true`, which would enforce all data to be sent to the server. Do note that for production this can lead to high costs, so it should only be used for debugging purposes.
Android ```kotlin val config = MeasureConfig( enableFullCollectionMode = true, ) Measure.init(context, config) ```
iOS ```swift let config = BaseMeasureConfig( enableFullCollectionMode: true ) Measure.initialize(with: clientInfo, config: config) ```
Flutter Flutter SDK depends on the native SDKs, so you need to set `enableFullCollectionMode` to `true` in both Android and iOS native SDK initializations.
### Verify API URL and API Key If you are not seeing any data in the dashboard, verify that the API URL and API key are set correctly in your app.
Android If logs show any of the following errors, make sure you have added the API URL and API key in your `AndroidManifest.xml` file. ``` sh.measure.android.API_URL is missing in the manifest sh.measure.android.API_KEY is missing in the manifest ```
iOS Verify the API URL and API key are set correctly in the `ClientInfo` object when initializing the SDK. ```swift let config = BaseMeasureConfig() let clientInfo = ClientInfo(apiKey: "", apiUrl: "") Measure.initialize(with: clientInfo, config: config) ```
Flutter Flutter SDK depends on the native SDKs, so verify that the API URL and API key are set correctly in both Android and iOS native SDK initializations.
React Native **Expo:** Verify that `androidApiKey`, `androidApiUrl`, `iosApiKey`, and `iosApiUrl` are all set in the plugin options in `app.json` and that `npx expo prebuild` has been run. **Vanilla React Native:** Verify the API key and URL are present in `AndroidManifest.xml` (Android) and that the upload build phase script has the correct API key and URL (iOS).
### Flutter iOS — MeasureSDK must be linked statically Flutter adds `use_frameworks!` to the iOS `Podfile` by default, which causes CocoaPods to link all pods dynamically. MeasureSDK must be linked statically and will not work correctly with dynamic linking. To fix this, follow the [CocoaPods static linking instructions](#using-cocoapods) in the iOS setup section. ### Connecting to Locally-hosted Server (for self-host customers) **iOS** If you are running the measure-sh server on your machine, setting the API_URL to localhost:8080 will work on the simulator because it can access localhost. However, a physical device cannot access your computer's localhost. To resolve this, you can use [ngrok](https://ngrok.com/) or a similar service to provide a public URL to your local server. This allows your physical device to connect to the server. **Android** For Android, if your device is on the same network as your computer, you can use your computer's local IP address (e.g., 192.168.1.X:8080) as the API_URL. Alternatively, you can set up ADB port forwarding with the command `adb reverse tcp: 8080 tcp:8080` to allow the device to connect to the server. When using an Android emulator, you can set the API_URL to http://10.0.2.2:8080 to access the server running on your machine. Alternatively, you can use [ngrok](https://ngrok.com/) or a similar service to provide a public URL to your local server. This allows your Android emulator or physical device to connect to the server. ### Enable Logs
Android Enable logging during SDK initialization. All Measure SDK logs use the tag `Measure`. ```kotlin val config = MeasureConfig(enableLogging = true) Measure.init(context, config) ```
iOS Enable logging during SDK initialization. ```swift let config = BaseMeasureConfig(enableLogging: true) Measure.initialize(with: clientInfo, config: config) ```
Flutter Enable logging during SDK initialization. ```dart await Measure.instance.init(() => runApp(MeasureWidget(child: MyApp())), config: const MeasureConfig(enableLogging:true)); ```
React Native Enable logging during SDK initialization. ```typescript const config = new MeasureConfig({ enableLogging: true }); await Measure.init({ config }); ```
### Connecting to a Self-hosted Server If you are hosting the server in cloud. Make sure the API URL is set to the public URL of your server. For example: set the API URL to `https://measure-api..com`, replacing with your own domain. ### Contact Support If none of the above steps resolve the issue, feel free to reach out to us on [Discord](https://discord.gg/f6zGkBCt42) for further assistance. ### Enable Diagnostic Mode If you're experiencing issues with the SDK and need to share detailed logs with us, enable diagnostic mode. This writes all internal SDK logs to files on disk which can then be pulled from the device and shared when reporting a bug. > [!NOTE] > These files only contain Measure SDK logs, not your app's logs. #### Step 1: Enable diagnostic mode
Android Enable diagnostic mode during SDK initialization. ```kotlin val config = MeasureConfig(enableDiagnosticMode = true) Measure.init(context, config) ```
iOS Enable diagnostic mode during SDK initialization. iOS provides an additional option called `enableDiagnosticModeGesture`. When this flag is enabled, you can use double finger double tap gesture to open share sheet and send logs immediately. ```swift let config = BaseMeasureConfig(enableDiagnosticMode: true, enableDiagnosticModeGesture: true) Measure.initialize(with: clientInfo, config: config) ```
Flutter Enable diagnostic mode during SDK initialization. ```dart await Measure.instance.init( () => runApp(MeasureWidget(child: MyApp())), config: const MeasureConfig(enableDiagnosticMode: true), ); ``` On iOS, the `enableDiagnosticModeGesture` flag is set on the native `BaseMeasureConfig` in your `AppDelegate` (see the iOS section above), not on the Dart `MeasureConfig`.
React Native Enable diagnostic mode during SDK initialization. ```typescript const config = new MeasureConfig({ enableDiagnosticMode: true }); await Measure.init({ config }); ``` On iOS, the `enableDiagnosticModeGesture` flag is set on the native `BaseMeasureConfig` in your `AppDelegate` (see the iOS section above), not on the Dart `MeasureConfig`.
#### Step 2: Reproduce the issue Run the app and reproduce the issue you're facing. The SDK will write logs to files in the app's internal storage. #### Step 3: Pull the log files
Android Use `adb` to retrieve the log files from the device: ```shell # List all diagnostic log files adb shell run-as ls files/measure/sdk_debug_logs/ # Pull all log files as a tar.gz archive adb shell "run-as tar czf - files/measure/sdk_debug_logs/" > /tmp/sdk_debug_logs.tar.gz ```
iOS iOS provides an option called `enableDiagnosticModeGesture`. When this flag is enabled, you can use double finger double tap gesture to open share sheet and send logs immediately. ```swift let config = BaseMeasureConfig(enableDiagnosticMode: true, enableDiagnosticModeGesture: true) Measure.initialize(with: clientInfo, config: config) ```
#### Step 4: Share the files Share the pulled log files on [Discord](https://discord.gg/f6zGkBCt42) or send them to us via email for us to investigate. #### Step 5: Disable diagnostic mode Once you've collected the logs, disable diagnostic mode by removing the `enableDiagnosticMode` flag or setting it to `false`. You can also delete the log files from the device:
Android ```shell adb shell run-as rm -rf files/measure/sdk_debug_logs/ ```
--- Source: https://measure.sh/docs/features/feature-session-timelines --- # Session Timeline * [**What Is a Session?**](#what-is-a-session) * [**Session Timeline**](#session-timeline) * [**Session Search**](#session-search) * [**API Reference**](#api-reference) * [**Get Current Session**](#get-current-session) ## What Is a Session? A session is a continuous period of activity within the app. A new session is created each time the SDK is initialized or when the app comes back to foreground after 30 seconds of being in background. ## Session Timeline The session timeline provides a detailed, chronological view of all events during a session. You can see the exact order of actions leading up to an issue, making it easier to spot problems. The timeline also includes memory and CPU usage graphs to help you understand how the app was performing at different moments. Additionally, screenshots and layout snapshots give you a window into what the user saw at the time. This visual context helps you catch UI issues, performance bottlenecks or anything else that might not be obvious just from logs. Session timelines are automatically collected for sessions with Crashes and Bug Reports. Each session timeline includes all events that occurred 5 minutes before the issue, giving you deep context on what led to the problem. See [Configuration Options](https://measure.sh/docs/configuration-options) for details on how to configure sampling rates for session timeline. ## Session Search You can easily find session timelines using attributes like user ID, session ID, device model, app version, OS version, country and more. You can also search based on specific events, such as when a view was clicked or a screen was visited. This is particularly useful for investigating user- or device-specific issues. The session search supports all custom events and attributes defined in your app, allowing you to filter sessions based on custom data like feature flags or user actions. > [!TIP] > > Construct complex queries easily using the filters UI and search box on the sessions page. For example, "find all sessions for 'premium' users in the 'US' using >'Android 16 or above', with the 'latest app version' that have a 'click' event on a view with ID 'btn_order_now'." ## API Reference ### Get Current Session You can retrieve the current session using the `getSessionId` method. This is useful when you want to send the session ID to a different service or log it for debugging purposes. #### Android ```kotlin val sessionId = Measure.getSessionId() ``` #### iOS Using Swift: ```swift let sessionId = Measure.getSessionId() ``` or, in Objective-C: ```objc NSString *sessionId = [Measure getSessionId]; ``` #### Flutter ```dart final String? sessionId = await Measure.instance.getSessionId(); ``` #### React Native ```ts const sessionId = await Measure.getSessionId(); ``` --- Source: https://measure.sh/docs/features/feature-crash-reporting --- # Crash Reporting Crashes are automatically tracked, optionally with a snapshot of the app's UI at the time of the crash. * [**Metrics**](#metrics) * [Crash-Free Rate](#crash-free-rate) * [Perceived Crash Rate](#perceived-crash-rate) * [**Get a UI Snapshot**](#get-a-ui-snapshot) * [**Crash Grouping**](#crash-grouping) * [**API Reference**](#api-reference) * [**Symbolicate Stacktrace**](#symbolicate-stacktrace) * [**How It Works**](#how-it-works) > [!NOTE] > Crash reporting for native crashes (from C/C++/etc.) is not yet supported. Track the progress [here](https://github.com/measure-sh/measure/issues/103). Upvote and comment on the issue if you are looking forward to this feature. ## Metrics Metrics related to crashes are automatically computed and shown on the dashboard. ![Crash metrics](../assets/crash-metrics.png) ### Crash-Free Rate The crash-free rate indicates the percentage of sessions that did not experience any crashes. It is calculated as follows: ``` Crash-Free Rate = (Total Sessions - Crashed Sessions) / Total Sessions * 100 ``` Where: - **Total Sessions**: The total number of sessions recorded. - **Crashed Sessions**: The number of sessions that experienced a crash. - **Crash-Free Rate**: The percentage of sessions that did not crash. The crash-free rate is a key metric to monitor the stability of your app. A higher crash-free rate indicates a more stable app, while a lower rate suggests that users are experiencing issues that need to be addressed. ### Perceived Crash Rate The perceived crash rate indicates the percentage of sessions that experienced a crash when the user was actively using the app. It is calculated as follows: ``` Perceived Crash Rate = Crashed Sessions When App Is in Foreground / Total Sessions * 100 ``` Where: - **Crashed Sessions When App Is in Foreground**: The number of sessions that experienced a crash while the app was actively being used by the user. - **Total Sessions**: The total number of sessions recorded. ## Get a UI Snapshot A screenshot of the app is captured when an app crashes. This feature is enabled by default and can be remotely configured on the dashboard under the "Apps" section. The following configuration options are available: - `Capture Screenshot on Crash` — Enables or disables the automatic screenshot capture on crash. It is enabled by default. - `Mask Sensitive Information` — Masks sensitive information in the screenshot by blurring text fields and password fields. It is disabled by default. Note that on iOS a screenshot cannot be captured reliably at the time of crash, it instead captures a layout snapshot. ## Crash Grouping Crashes are grouped to help you identify the most common issues in your app. Each group represents a unique crash, identified by the exception type and the stack trace. A percentage contribution of each crash group is also shown on the dashboard to get a quick idea about the impact of the issue. ### Android Exceptions in Android (JVM) are grouped based on the exception type and the stack trace. Crashes with the same type and the same method name and file name from the _first frame_ of the stack trace are grouped together. For example, for the following crash, the exception type is `java.lang.NullPointerException`, the method name of the first frame is `onCreate` and the file name is `MainActivity.kt`: ``` 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 Exceptions in iOS (Objective-C/Swift) are grouped based on the exception signal and the stack trace. Crashes with the same signal, for example, `SIGABRT`, the same method name and file name from the _first frame belonging to the application binary_ are grouped together. For example, for the following crash, the signal is `SIGABRT`, the method name of the first relevant frame is `-viewDidLoad` and the file name is `MainViewController.m`: ``` 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 In Flutter, crashes are grouped based on the exception type and the stack trace. Crashes with the same exception type and the same method name and file name from the _first frame_ of the stack trace are grouped together. For example, for the following crash, the exception type is `FlutterError`, the method name of the first frame is `_incrementCounter` and the file name is `main.dart`: ``` 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 In React Native, JS exceptions are grouped based on the error message and the stack trace. Crashes with the same message and the same function name and file name from the _first frame_ of the stack trace are grouped together. ## API Reference ### Symbolicate Stacktrace Stack traces from crashes may be obfuscated or contain memory addresses. To convert the stack traces to a human-readable format, you need to upload the mapping or symbol files based on the platform. #### Android If you are using ProGuard or R8 to obfuscate your code, you need to upload the mapping files to de-obfuscate the stack traces. Measure's Android Gradle Plugin automatically uploads the ProGuard/R8 mapping file to the Measure server when you run an `assemble` gradle task. #### iOS To symbolicate stack traces for iOS, you need to upload the dSYM files to map the memory addresses to a human-readable format. There are two ways to upload dSYM files: ##### Using Shell Script Run the [`upload_dsym_manual.sh`](../../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 Run the [`upload_dsym_xcarchive.sh`](../../ios/Scripts/upload_dsym_xcarchive.sh) script to automatically upload dSYMs using `xcarchive` file. This script automatically extracts all necessary build metadata (version, build size, dSYM paths, etc.) directly from the generated .xcarchive. the `ipa` file is necessary to generate the build size info. If the ipa_path is not provided, the script uses the application binary to generate build size. ```sh ./upload_dsym_xcarchive.sh [custom_headers] [ipa_path] ``` > [!CAUTION] > If you are using Build Phases to upload DSYMs, make sure to **upload DSYMs only for release builds**. #### React Native React Native apps require sourcemaps to symbolicate JavaScript stack traces. The setup differs slightly between Android and iOS. ##### Android The Measure Android Gradle Plugin automatically captures and uploads the composed JavaScript sourcemap when you run `assembleRelease` or `bundleRelease`. No additional steps are required. > [!IMPORTANT] > Always use the sourcemap from the Gradle build output. Generating the sourcemap separately (e.g. via `npx react-native bundle`) produces a different bundle than what is embedded in the APK and will result in incorrect symbolication. ##### iOS Add the `upload_build_phase.sh` script as a Run Script Build Phase in Xcode. It automatically uploads dSYM files and JavaScript sourcemaps in one step. **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. > [!IMPORTANT] > Always use the sourcemap generated by the Xcode build (via `SOURCEMAP_FILE`). Generating the sourcemap manually produces a different bundle than what is embedded in the app and will result in incorrect symbolication. > [!NOTE] > 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 When obfuscating your Flutter app using --obfuscate and --split-debug-info options: * **Android** — The Measure Android Gradle Plugin automatically uploads the required mapping files. * **iOS** — You need to upload the dSYM files as described in the iOS section above. After building with `flutter build ipa`, run the `upload_dsyms.sh` script using the IPA path and the path to the dSYM folder (typically under _/build/ios/Release-iphoneos/_) ## How It Works ### Android When an unhandled exception occurs, it is intercepted by Measure using an `UncaughtExceptionHandler`. The exception is then parsed and sent as an `exception` event. Crashes are attempted to be sent immediately, but if the app is terminated before the event is sent, it is stored locally and sent on the next app launch. ### iOS We rely on [PLCrashReporter](https://github.com/microsoft/plcrashreporter) to detect crashes. When a crash occurs, a crash report is generated and stored locally on the device. On the next launch, the crash report is prepared and sent to the server. ### Flutter When the SDK is initialized, it automatically sets up both `FlutterError.onError` and `PlatformDispatcher.instance.onError` callbacks. Any errors are forwarded to the server. Internally, we rely on [stack_trace](https://pub.dev/packages/stack_trace) to parse the stack trace and send it as an `exception` event. All crashes captured by the native Android or iOS SDK are also tracked for Flutter apps. ### React Native When the SDK is initialized, it automatically installs a global error handler via React Native's `ErrorUtils` and sets up an unhandled Promise rejection tracker. Any unhandled JS exceptions and promise rejections are captured and sent as `exception` events. All crashes captured by the underlying native Android or iOS SDKs are also tracked for React Native apps. --- Source: https://measure.sh/docs/features/feature-anr-reporting --- # ANR Reporting * [**Debugging ANRs**](#debugging-anrs) * [**Metrics**](#metrics) * [**Get a UI Screenshot**](#get-a-ui-screenshot) * [**How It Works**](#how-it-works) ANRs (Application Not Responding errors) are automatically tracked. Optionally, the SDK can also capture a screenshot of your app’s UI at the moment the crash occurred. ## Debugging ANRs You can easily debug ANRs by following the in-depth guide in the [Android documentation](https://developer.android.com/topic/performance/anrs/diagnose-and-fix-anrs). It covers different scenarios that lead to ANRs and how to approach fixing them. You can also use the session timelines in Measure to see what was happening in the app before the ANR occurred. The timeline shows all app activity, including CPU and Memory usage, giving you crucial context for understanding the ANR and pinpointing the cause. Additionally, starting from Android API level 30, the Application Exit feature provides valuable insights. Measure tracks all app exit events and reports them to the server along with the stack trace. Here’s an example snippet from a trace: ``` "main" prio=5 tid=1 Blocked at sh.measure.sample.ExceptionDemoActivity.deadLock$lambda$10(ExceptionDemoActivity.kt:66) - waiting to lock <0x0a293e9f> (a java.lang.Object) held by thread 22 at sh.measure.sample.ExceptionDemoActivity.$r8$lambda$kc26SdTV_Hqz6i5PLOpVXKS016U(unavailable:0) at sh.measure.sample.ExceptionDemoActivity$$ExternalSyntheticLambda9.run(unavailable:2) at android.os.Handler.handleCallback(Handler.java:942) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:201) at android.os.Looper.loop(Looper.java:288) at android.app.ActivityThread.main(ActivityThread.java:7872) at java.lang.reflect.Method.invoke(Native method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) "APP: Locker" prio=5 tid=22 Sleeping at java.lang.Thread.sleep(Native method) - sleeping on <0x06290e31> (a java.lang.Object) at java.lang.Thread.sleep(Thread.java:450) - locked <0x06290e31> (a java.lang.Object) at java.lang.Thread.sleep(Thread.java:355) at sh.measure.sample.ExceptionDemoActivity.sleep(ExceptionDemoActivity.kt:86) at sh.measure.sample.ExceptionDemoActivity.access$sleep(ExceptionDemoActivity.kt:12) at sh.measure.sample.ExceptionDemoActivity$LockerThread.run(ExceptionDemoActivity.kt:80) - locked <0x0a293e9f> (a java.lang.Object) ``` In this example, the “main” thread is blocked, waiting to lock an object held by _thread 22_, which is sleeping and holding that lock. This creates a deadlock, leading to an ANR. > [!NOTE] > Starting from API 31, for native crashes, App Exit Info contains the [tombstone](https://source.android.com/docs/core/tests/debug) stack trace. Support for native crash reports is not yet implemented and will be coming soon. Track the updates in this [issue](https://github.com/measure-sh/measure/issues/103). ## Metrics ![ANR-Free Rate](../assets/anr-metrics.png) The ANR-Free Rate measures the percentage of sessions that didn’t experience any ANRs. Here’s how it’s calculated: ``` ANR-Free Rate = (Total Sessions - ANR Sessions) / Total Sessions * 100 ``` Where: - **Total Sessions**: The total number of sessions recorded. - **ANR Sessions**: The number of sessions that experienced an ANR. ## Get a UI Screenshot A screenshot of the app is captured when an app crashes. This feature is enabled by default and can be remotely configured on the dashboard under the "Apps" section. The following configuration options are available: - `Capture Screenshot on ANR` — Enables or disables the automatic screenshot capture on crash. It is enabled by default. - `Mask Sensitive Information` — Masks sensitive information in the screenshot by blurring text fields and password fields. It is disabled by default. ## How It Works Measure SDK detects ANRs by tracking the `SIGQUIT` signal. When an ANR occurs, this signal is sent to the app process. The SDK picks up on this signal and reports the ANR to the server. #### Implementation Details * [Signal](#handling-signals) * [Handling Signals](#handling-signals) * [Semaphores](#semaphores) * [Detecting SIGQUIT on Android](#detecting-sigquit-on-android) Apart from setting a signal disposition using [sigaction](https://man7.org/linux/man-pages/man2/sigaction.2.html), a thread can also set up a [sigwait](https://man7.org/linux/man-pages/man3/sigwait.3.html). This allows the registered signals to be handled on this thread without triggering the `sigaction` handler. ## Handling Signals Signals can be *handled* using [sigaction](https://man7.org/linux/man-pages/man2/sigaction.2.html) or [sigwait](https://man7.org/linux/man-pages/man3/sigwait.3.html) (among others which we'll not cover in this article). ### sigaction(2) The `sigaction` system call is used to change the action taken by a process on receipt of a specific signal. The handler function provided affects how the process handles the signal (i.e., signal handler is not per thread). > Signal handlers are per-process, but signal masks are per-thread. ### sigwait(3) The `sigwait` function suspends execution of the calling thread until one of the signals specified in the signal set becomes `pending`. **Once a signal is processed by this thread, it is automatically cleared from the set of pending signals**. It then returns that signal number. No signal handlers set using `sigaction` are triggered for this signal once cleared by `sigwait`. `sigwait` makes it easy to handle the signals synchronously in normal program execution. ## Semaphores The implementation relies on Semaphores to synchronize the threads. ### sem_wait(3) `sem_wait` decrements (locks) the semaphore. If the semaphore's value is greater than zero, then the decrement proceeds, and the function returns immediately. If the semaphore currently has the value zero, then the call blocks until it becomes possible to perform the decrement (i.e., the semaphore value rises above zero). ### sem_post(3) `sem_post` increments (unlocks) the semaphore. If the semaphore's value consequently becomes greater than zero, then the thread blocked in a `sem_wait` call will be woken up and proceed to lock the semaphore. ## Detecting SIGQUIT on Android Setting a signal handler using `sigaction` is the typical way to detect and track a `SIGQUIT` on a Linux system. However, Android registers [sigwait](https://man7.org/linux/man-pages/man3/sigwait.3.html) for the `SIGQUIT` signal for every app ([reference](https://android.googlesource.com/platform/art/+/refs/heads/main/runtime/signal_catcher.cc)). Hence, simply registering a `sigaction` will not have any effect as noted earlier. 1. Find the thread ID of the thread called "[Signal Catcher](https://android.googlesource.com/platform/art/+/refs/heads/main/runtime/signal_catcher.cc)". This is the thread using which Android registers the `sigwait`. Thread ID is found by looping over the directory `/proc//task/` and reading the thread name from the contents of the `comm` file in each subdirectory to find the thread name. More on this can be found [here](https://man.freebsd.org/cgi/man.cgi?apropos=0&arch=default&format=html&manpath=CentOS+7.1&query=proc&sektion=5#:~:text=%2Fproc%2F%5Bpid%5D%2Ftask%09(since%20Linux%202.6.0%2Dtest6)). 2. Create a new thread called "Watchdog" using `pthread_create` and set the signal mask to unblock the `SIGQUIT` signal using `pthread_sigmask` for this thread. This ensures that the new thread can receive the `SIGQUIT` signal instead of the _Signal Catcher_ thread. 3. Register a semaphore using [sem_wait](https://man7.org/linux/man-pages/man3/sem_wait.3.html) in the _Watchdog_ thread which blocks it until the `SIGQUIT` signal is received. 4. Register a signal handler using `sigaction` for the `SIGQUIT` signal. When `SIGQUIT` is received, the handler invokes [sem_post](https://man7.org/linux/man-pages/man3/sem_post.3.html) to unblock the _Watchdog_ thread. 5. When the _Watchdog_ thread's semaphore is unlocked, it notifies Measure SDK of an ANR by a callback using JNI. 6. Eventually, the Watchdog thread notifies the "Signal Catcher" thread to handle the signal as usual by using `syscall`: ```c syscall(SYS_tgkill, pid, signal_catcher_tid, SIGQUIT); ``` --- Source: https://measure.sh/docs/features/feature-error-tracking --- # Track errors Measure automatically captures crashes and ANRs (Application Not Responding) in your app, but you can also track handled exceptions that occur during normal app operation. This is useful for identifying issues that may not crash the app but still affect user experience. - [**API Reference**](#api-reference) - [**Android**](#android) - [**iOS**](#ios) - [**Flutter**](#flutter) - [**React Native**](#react-native) ## API Reference #### Android To track handled exceptions, use the `trackHandledException`method. ```kotlin try { methodThatThrows() } catch (e: Exception) { Measure.trackHandledException(e) } ``` From version `0.12.0` onwards you can also add attributes to the tracked exception, which can be useful for providing additional context about the error. - Attribute keys must be strings with a maximum length of 256 characters. - Attribute values must be one of the primitive types: `int`, `long`, `double`, `float` or `boolean`. - String attribute values can have a maximum length of 256 characters. ```kotlin try { methodThatThrows() } catch (e: Exception) { val attributes = AttributesBuilder().put("screen", "Login") .put("retryCount", 2) .build() Measure.trackHandledException(e, attributes) } ``` #### iOS To track handled errors or exceptions, use the `trackError` method with either a native Swift Error or an NSError. ```swift do { try someThrowingFunction() } catch { Measure.trackError(error) } ``` You can optionally include attributes and enable stack trace collection. - Attribute keys must be strings with a maximum length of 256 characters. - Attribute values must be one of the primitive types: `int`, `long`, `double`, `float` or `boolean`. - String attribute values can have a maximum length of 256 characters. ```swift Measure.trackError(error, attributes: [ "screen": .string("Login"), "retryCount": .int(2) ]) ``` You can track handled NSError objects from Objective-C code as well using `trackError` method. ```objc [Measure trackError:error attributes:@{ @"screen": @"Login", @"retryCount": 2 }]; ``` #### Flutter To track handled exceptions in Flutter, use the `trackHandledError` method from the Measure SDK. ```dart try { methodThatThrows(); } catch (e, stackTrace) { Measure.trackHandledError(e, stackTrace); } ``` #### React Native To track handled errors, use the `trackError` method from the Measure SDK. ```ts import { Measure } from '@measuresh/react-native'; try { methodThatThrows(); } catch (e) { Measure.trackError({ error: e }); } ``` You can optionally include attributes to provide additional context about the error. - Attribute keys must be strings with a maximum length of 256 characters. - Attribute values must be one of the primitive types: `string`, `number`, or `boolean`. - String attribute values can have a maximum length of 256 characters. ```ts try { methodThatThrows(); } catch (e) { Measure.trackError({ error: e, attributes: { screen: 'Login', retryCount: 2, }, }); } ``` --- Source: https://measure.sh/docs/features/feature-gesture-tracking --- # Gesture Tracking * [**Overview**](#overview) * [**Layout Snapshots**](#layout-snapshots) * [**Flutter**](#flutter) * [**How it works**](#how-it-works) * [**Android**](#android) * [**iOS**](#ios) * [**Flutter**](#flutter-1) * [**Benchmark Results**](#benchmark-results) * [**Android**](#android-1) * [**iOS**](#ios-1) ## Overview Measure SDK captures gestures like click, long click and scroll events automatically. These events make it easy to understand user interactions with your app without having to manually instrument every view. Additionally, a layout snapshot is captured at the time of the gesture, which helps in understanding the context of the gesture and the state of the UI at that moment. ## Layout Snapshots Layout snapshots provide a lightweight way to capture the structure of your UI at key user interactions. They are automatically collected during click events (with throttling) and store the layout hierarchy as a wireframe rather than full screenshots. This approach gives valuable context about the UI state during user interactions while being significantly more efficient to capture and store than traditional screenshots. | Screenshot | Layout snapshot | |------------------------------------------------------|------------------------------------------------| | ![Screenshot](../assets/layout-snapshot-screenshot.png) | ![Layout Snapshot](../assets/layout-snapshot.png) | Layout snapshots are captured along with every gesture click event with throttling (750ms between consecutive snapshots). This ensures that you get a representative snapshot of the UI without overwhelming the system with too many images. The snapshots are stored in a compressed lightweight format, which is efficient for both storage and rendering. ### Flutter Layout snapshots are collected by default for Flutter applications. However, only the widgets shown in the table are used to build the layout snapshot. To make the layout snapshot more useful, you can use a build time script to also add any other widgets that are used in your application. See [measure_build](https://measure.sh/docs/../../flutter/packages/measure_build) package for more details. | Default Widget Types | |-------------------------| | `FilledButton` | | `OutlinedButton` | | `TextButton` | | `ElevatedButton` | | `CupertinoButton` | | `ButtonStyleButton` | | `MaterialButton` | | `IconButton` | | `FloatingActionButton` | | `ListTile` | | `PopupMenuButton` | | `PopupMenuItem` | | `DropdownButton` | | `DropdownMenuItem` | | `ExpansionTile` | | `Card` | | `Scaffold` | | `CupertinoPageScaffold` | | `MaterialApp` | | `CupertinoApp` | | `Container` | | `Row` | | `Column` | | `ListView` | | `PageView` | | `SingleChildScrollView` | | `ScrollView` | | `Text` | | `RichText` | ## How it works ### Android When Measure SDK is initialized, it registers a touch event interceptor using the [Curtains library](https://github.com/square/curtains?tab=readme-ov-file#curtainskt). It allows Measure to intercept every touch event in an application and process it. There are two main parts to tracking gestures: 1. [Gesture detection](#gesture-detection) 2. [Gesture target detection](#gesture-target-detection) #### Gesture detection Measure tracks the time between `ACTION_DOWN` and `ACTION_UP` events and the distance moved to classify a touch as click, long click or scroll. A gesture is classified as a long click gesture if the time interval between `ACTION_DOWN` and `ACTION_UP` is more than [ViewConfiguration.getLongPressTimeout](https://developer.android.com/reference/android/view/ViewConfiguration#getLongPressTimeout()) time and the distance moved by the pointer between the two events is less than [ViewConfiguration.get.getScaledTouchSlop](https://developer.android.com/reference/android/view/ViewConfiguration#getScaledTouchSlop()). A gesture is classified as a click if the distance moved by the pointer between the two events is less than [ViewConfiguration.get.getScaledTouchSlop](https://developer.android.com/reference/android/view/ViewConfiguration#getScaledTouchSlop()) but the time interval between the two events is less than [ViewConfiguration.getLongPressTimeout()](https://developer.android.com/reference/android/view/ViewConfiguration#getLongPressTimeout()). A gesture is classified as a scroll if the distance moved by the pointer between the two events is more than [ViewConfiguration.get.scaledTouchSlop](https://developer.android.com/reference/android/view/ViewConfiguration#getScaledTouchSlop()). An estimation of direction in which the scroll happened based on the pointer movement. **For compose**, a click/long click is detected by traversing the semantics tree using [SemanticsOwner.getAllSemanticsNodes](https://developer.android.com/reference/kotlin/androidx/compose/ui/semantics/SemanticsOwner#(androidx.compose.ui.semantics.SemanticsOwner).getAllSemanticsNodes(kotlin.Boolean,kotlin.Boolean)) and finding a composable at the point where the touch happened and checking for Semantics Properties - SemanticsActions.OnClick, SemanticsActions.OnLongClick and SemanticsActions.ScrollBy for click, long click and scroll respectively. > [!NOTE] > > Compose currently reports the target_id in the collected data using [testTag](https://developer.android.com/reference/kotlin/androidx/compose/ui/semantics/package-summary#(androidx.compose.ui.semantics.SemanticsPropertyReceiver).testTag()), > if it is set. While the `target` is always reported as `AndroidComposeView`. #### Gesture target detection Along with the type of gesture which occurred, Measure can also **estimate** the target view/composable on which the gesture was performed on. For a click/long click, a hit test is performed to check the views which are under the point where the touch occurred. A traversal is performed on the children of the view group found and is checked for any view which has either [isClickable](https://developer.android.com/reference/android/view/View#isClickable()) or [isPressed](https://developer.android.com/reference/android/view/View#isPressed()) set to true. If one is found, it is returned as the target, otherwise, the touch is discarded as can be classified as a "dead click". Similarly, for a scroll, after the hit test, a traversal is performed for any view which has [isScrollContainer](https://developer.android.com/reference/android/view/View#isScrollContainer()) set to true and [canScrollVertically](https://developer.android.com/reference/android/view/View#canScrollVertically(int)) or [canScrollHorizontally](https://developer.android.com/reference/android/view/View#canScrollHorizontally(int)). If a view which satisfies this condition it is returned as the target, otherwise, the scroll is discarded and can be classified as a "dead scroll". Gesture tracking consists of two main components: 1. [Gesture detection](#gesture-detection) 2. [Gesture target detection](#gesture-target-detection) ### iOS #### Gesture detection Measure SDK detects touch events by swizzling `UIWindow`'s `sendEvent` method. It processes touch events to classify them into different gesture types: - **Click**: A touch event that lasts for less than 500 ms. - **Long Click**: A touch event that lasts for more than 500 ms. - **Scroll**: A touch movement exceeding 3.5 points in any direction. #### Gesture target detection Gesture target detection identifies the UI element interacted with during a gesture. It first determines the view at the touch location and then searches its subviews to find the most relevant target. For scroll detection, it checks if the interacted element is a scrollable view like `UIScrollView`, `UIDatePicker` or `UIPickerView`. ### Flutter #### Gesture detection Measure SDK detects touch events by listening to pointer events from [Listener](https://api.flutter.dev/flutter/widgets/Listener-class.html) widget, which is added to the root widget of the app using `MeasureWidget`. It processes touch events to classify them into different gesture types: - **Click**: A touch event that lasts for less than 500 ms. - **Long Click**: A touch event that lasts for more than 500 ms. - **Scroll**: A touch movement exceeding 20 pixels in any direction. #### Gesture target detection The SDK automatically identifies gesture targets by traversing the widget tree and checking if widgets at the touch position are interactive. For clicks and long clicks, it searches for clickable widgets, while for scrolls, it looks for scrollable widgets. > [!NOTE] > Any widget not listed in the tables below will not be automatically tracked for > gestures. For custom widgets or unsupported widget types, gestures will not be detected unless they > inherit from or contain one of the supported widget types. **Supported Clickable Widgets:** | Widget Type | |------------------------| | `ButtonStyleButton` | | `MaterialButton` | | `IconButton` | | `FloatingActionButton` | | `CupertinoButton` | | `ListTile` | | `PopupMenuButton` | | `PopupMenuItem` | | `DropdownButton` | | `DropdownMenuItem` | | `ExpansionTile` | | `Card` | | `GestureDetector` | | `InputChip` | | `ActionChip` | | `FilterChip` | | `ChoiceChip` | | `Checkbox` | | `Switch` | | `Radio` | | `CupertinoSwitch` | | `CheckboxListTile` | | `SwitchListTile` | | `RadioListTile` | | `TextField` | | `TextFormField` | | `CupertinoTextField` | | `Stepper` | **Supported Scrollable Widgets:** | Widget Type | |-------------------------| | `ListView` | | `ScrollView` | | `PageView` | | `SingleChildScrollView` | ### Layout snapshots Layout snapshots capture your app's UI structure by traversing the widget tree from the root widget. The SDK collects key information about each widget—including its type, position, size and hierarchy—to build a lightweight representation of your UI. The entire layout snapshot is generated in a single pass through the widget tree using the `visitChildElements` method. Since a typical Flutter screen can contain thousands of widgets, the snapshot is optimized to include only relevant widget types to maintain performance and clarity. To include custom widgets or additional widget types in your snapshots, use the [measure_build](https://measure.sh/docs/../../flutter/packages/measure_build) package to generate a comprehensive list of all widget types used in your app. ### React Native Gesture detection for React Native apps are based on the platform the app is running on (Android or iOS). The SDK uses the same methods as described above for Android and iOS. ## Benchmark results ### Android Checkout the results from a macro benchmark we ran for gesture target detection [here](https://github.com/measure-sh/measure/pull/377#issue-2123559330). TLDR; * On average, it takes 0.458 ms to find the clicked view in a deep view hierarchy. * On average, it takes 0.658 ms to find the clicked composable in a deep composable hierarchy. ### iOS - On average, it takes **4 ms** to identify the clicked view in a view hierarchy with a depth of **1,500**. - For more common scenarios, a view hierarchy with a depth of **20** takes approximately **0.2 ms**. ### Flutter - On average, it takes **3ms** to generate a layout snapshot and identify the clicked widget in a widget tree with a depth of **50** widgets. - The time to generate the layout snapshot increases linearly with the depth of the widget tree. - The benchmark tests can be found in [Layout Snapshot Perf Tests](../../flutter/example/integration_test/layout_snapshot_performance_test.dart). ### Flutter - On average, it takes **10ms** to generate a layout snapshot and identify the clicked widget in a widget tree with a depth of **100** widgets. - The time to generate the layout snapshot increases linearly with the depth of the widget tree. - The benchmark tests can be found [here](../../flutter/example/integration_test/layout_snapshot_performance_test.dart). --- Source: https://measure.sh/docs/features/feature-performance-tracing --- # Performance Tracing * [**Introduction**](#introduction) * [**Concepts**](#concepts) * [**API Reference**](#api-reference) * [Limits](#limits) * [Start a Span](#start-a-span) * [End a Span](#end-a-span) * [Set Parent Span](#set-parent-span) * [Set Attributes](#set-attributes) * [Remove Attribute](#remove-attribute) * [Update Span Name](#update-span-name) * [Add Checkpoint](#add-checkpoint) * [Deferred Span Start](#deferred-span-start) * [Distributed Tracing](#distributed-tracing) * [Get a Trace Parent Header](#get-a-trace-parent-header) * [**Recipes**](#recipes) * [Distributed Tracing with OkHttp Interceptor](#distributed-tracing-with-okhttp-interceptor) * [Distributed Tracing with URLSession Interceptor](#distributed-tracing-with-urlsession-interceptor) * [**Screen Load Time**](#screen-load-time) * [Android](#android) * [How It Works](#how-it-works) * [Data Captured](#data-captured) * [Spans Visualized Using Perfetto](#spans-visualized-using-perfetto) * [iOS](#ios) * [How It Works](#how-it-works-1) * [Data Captured](#data-captured-1) ## Introduction You can easily track the performance of any part of your application, such as API calls, DB queries, any function or a user journey, using the performance tracing APIs. The SDK supports nested spans to track hierarchical operations. > [!NOTE] > > measure-sh can automatically track traces for screen load times in Android and iOS apps for _Activities_, _Fragments_ and _UIViewControllers_. See [Screen Load Time](#screen-load-time) for more details. Here's a simple example of how to use the performance tracing APIs:
Android ```kotlin val onboardingSpan = Measure.startSpan("onboarding-flow") try { val signupSpan = Measure.startSpan("signup", parent = onboardingSpan) userSignup() signupSpan.end() val tutorialSpan = Measure.startSpan("tutorial", parent = onboardingSpan) showTutorial() tutorialSpan.end(SpanStatus.Ok) } finally { onboardingSpan.end(SpanStatus.Error) } ```
iOS ```swift let onboardingSpan: Span = Measure.startSpan(name: "onboarding-flow") do { let signupSpan = Measure.startSpan(name: "signup").setParent(onboardingSpan) userSignup() signupSpan.end() let tutorialSpan = Measure.startSpan(name: "tutorial").setParent(onboardingSpan) showTutorial() tutorialSpan.end(status: .ok) } catch { onboardingSpan.end(status: .error) } ```
Flutter ```dart final onboardingSpan = Measure.instance.startSpan("onboarding-flow"); try { final signupSpan = Measure.instance .startSpan("signup") .setParent(onboardingSpan); await userSignup(); signupSpan.end(); final tutorialSpan = Measure.instance .startSpan("tutorial") .setParent(onboardingSpan); await showTutorial(); tutorialSpan.end(status: SpanStatus.ok); } catch (e) { onboardingSpan.end(status: SpanStatus.error); } finally { onboardingSpan.end(); } ```
React Native ```typescript import { Measure } from '@measuresh/react-native'; const onboardingSpan = Measure.startSpan({ name: "onboarding-flow" }); try { const signupSpan = Measure.startSpan({ name: "signup" }).setParent(onboardingSpan); await userSignup(); signupSpan.end(); const tutorialSpan = Measure.startSpan({ name: "tutorial" }).setParent(onboardingSpan); await showTutorial(); tutorialSpan.end(); } catch (e) { onboardingSpan.setAttribute("error", true); } finally { onboardingSpan.end(); } ```
This will result in a trace like the following: ``` onboarding-flow ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [2.4s] ✓ ┃ ┣━ signup ━━━━━━━━━ [800ms] ✓ ┃ ┗━ tutorial ━━━━━━━━━━━━━━━━ [1.6s] ✓ ``` ## Concepts Tracing helps you understand how long certain operations take to complete, from the moment they begin until they finish, including all the intermediate steps, dependencies and parallel activities that occur during execution. A **trace** represents the entire operation, which could be a complete user journey like onboarding, further divided into multiple steps like login, create profile, etc. A trace is represented by a `trace_id`. A **span** is the fundamental building block of a trace. A span represents a single unit of work. This could be an HTTP request, a database query, a function call, etc. Each span contains information about the operation — when it started, how long it took and whether it completed successfully or not. A span is identified using a `span_id` and a user-defined `name`. Each span can optionally have a **parent span**, which allows you to create a hierarchy of spans. This is useful for tracking nested operations. For example, if a user journey involves multiple steps, each step can be represented as a child span of the main user journey span. ![Trace view](../assets/trace-view.png) ## API Reference ### Limits The following limits apply to spans. Spans violating the limits will either be discarded or have their data truncated. | | Limit | |----------------------------|-------| | Max Span Name Length | 64 | | Max Checkpoint Name Length | 64 | | Max Checkpoints Per Span | 100 | ### Start a Span A span can be started using the `startSpan` function.
Android ```kotlin val span: Span = Measure.startSpan("span-name") ```
iOS ```swift let span: Span = Measure.startSpan(name: "span-name") ```
Flutter ```dart final span = Measure.instance.startSpan("span-name"); ```
React Native ```typescript const span = Measure.startSpan({ name: "span-name" }); ```
A span can also be started by providing the start time, this is useful in cases where a certain operation has already started but there wasn't any way to access the Measure APIs in that part of the code. > [!IMPORTANT] > To set the start time, use `Measure.getCurrentTime`, which returns epoch time calculated using a > monotonic clock. > Passing in `System.currentTimeInMillis` can lead to issues with corrupted span timings due to > clock skew issues.
Android ```kotlin val span: Span = Measure.startSpan("span-name", timestamp = Measure.getCurrentTime()) ```
iOS ```swift let span: Span = Measure.startSpan(name: "operation-name", timestamp: Measure.getCurrentTime()) ```
Flutter ```dart final span = Measure.instance.startSpan("span-name", timestamp: Measure.instance.getCurrentTime()); ```
React Native ```typescript const span = Measure.startSpanWithTimestamp({ name: "span-name", timestampMs: Measure.getCurrentTime() }); ```
### End a Span A span can be ended using the `end` function. Status is mandatory to set when ending a span.
Android ```kotlin val span: Span = Measure.startSpan("span-name") span.setStatus(SpanStatus.Ok).end() ```
iOS ```swift let span: Span = Measure.startSpan(name: "span-name") span.setStatus(.ok).end() ```
Flutter ```dart final span = Measure.instance.startSpan("span-name"); span.setStatus(SpanStatus.ok).end(); ```
React Native ```typescript const span = Measure.startSpan({ name: "span-name" }); span.end(); ```
A span can also be ended by providing the end time, this is useful in cases where a certain operation has already ended but there wasn't any way to access the Measure APIs in that part of the code. > [!IMPORTANT] > To set the end time, use `Measure.getCurrentTime`, which returns epoch time calculated using a > monotonic clock. > Passing in `System.currentTimeInMillis` can lead to issues with corrupted span timings due to > clock skew issues.
Android ```kotlin val span: Span = Measure.startSpan("span-name") span.setStatus(SpanStatus.Ok).end(timestamp = Measure.getCurrentTime()) ```
iOS ```swift let span: Span = Measure.startSpan(name: "span-name") span.setStatus(.ok).end(timestamp: Measure.getCurrentTime()) ```
Flutter ```dart final span = Measure.instance.startSpan("span-name"); span.setStatus(SpanStatus.ok).end(timestamp: Measure.instance.getCurrentTime()); ```
React Native ```typescript const span = Measure.startSpan({ name: "span-name" }); span.end(Measure.getCurrentTime()); ```
### Set Parent Span To set a parent span, use the `setParent` method.
Android ```kotlin val parentSpan: Span = Measure.startSpan("parent-span") val childSpan: Span = Measure.startSpan("child-span").setParent(parentSpan) ```
iOS ```swift let parentSpan: Span = Measure.startSpan(name: "parent-span") let childSpan: Span = Measure.startSpan(name: "child-span").setParent(parentSpan) ```
Flutter ```dart final parentSpan = Measure.instance.startSpan("parent-span"); final childSpan = Measure.instance.startSpan("child-span").setParent(parentSpan); ```
React Native ```typescript const parentSpan = Measure.startSpan({ name: "parent-span" }); const childSpan = Measure.startSpan({ name: "child-span" }).setParent(parentSpan); ```
### Set Attributes Attributes are key-value pairs that can be attached to a span. Attributes are used to add additional context to a span. > [!NOTE] > - Attribute keys must be strings with a maximum length of `256` characters. > - Attribute keys must only contain alphabets, numbers, hyphens and underscores. > - Attribute values must be one of the primitive types: `int`, `long`, `double`, `float` or `boolean`. > - String attribute values can have a maximum length of `256` characters. To add attributes to a span, use `setAttribute`.
Android ```kotlin val span: Span = Measure.startSpan("span-name") span.setAttribute("key", "value") ```
iOS ```swift let span: Span = Measure.startSpan(name: "span-name") span.setAttribute("key", "value") ```
Flutter ```dart final span = Measure.instance.startSpan("span-name"); span.setAttributeString("key", "value"); span.setAttributeInt("key", 10); span.setAttributeDouble("key", 10.5); span.setAttributeBool("key", true); ```
React Native ```typescript const span = Measure.startSpan({ name: "span-name" }); span.setAttribute("key", "value"); span.setAttribute("count", 10); span.setAttribute("enabled", true); ```
To add multiple attributes at once use `setAttributes`.
Android ```kotlin val span: Span = Measure.startSpan("span-name") val attributes = AttributesBuilder().put("key", "value").put("key2", "value2").build() span.setAttributes(attributes) ```
iOS ```swift let span: Span = Measure.startSpan(name: "span-name") let attributes: [String: AttributeValue] = ["key": "value", "key2": 42] span.setAttributes(attributes) ```
Flutter ```dart final span = Measure.instance.startSpan("span-name"); final attributes = AttributeBuilder().add("key", "value").add("key2", 42).build(); span.setAttributes(attributes); ```
React Native ```typescript const span = Measure.startSpan({ name: "span-name" }); span.setAttributes({ key: "value", key2: 42, enabled: true }); ```
### Remove Attribute To remove an attribute use `removeAttribute`.
Android ```kotlin val span: Span = Measure.startSpan("span-name") span.removeAttribute("key") ```
iOS ```swift let span: Span = Measure.startSpan(name: "span-name") span.removeAttribute("key") ```
Flutter ```dart final span = Measure.instance.startSpan("span-name"); span.removeAttribute("key"); ```
React Native ```typescript const span = Measure.startSpan({ name: "span-name" }); span.removeAttribute("key"); ```
### Update Span Name To update the name of the span after it is started, use `setName`.
Android ```kotlin val span: Span = Measure.startSpan("span-name") span.setName("updated-name").end() ```
iOS ```swift let span: Span = Measure.startSpan(name: "span-name") span.setName("updated-name").end() ```
Flutter ```dart final span = Measure.instance.startSpan("span-name"); span.setName("updated-name").end(); ```
React Native ```typescript const span = Measure.startSpan({ name: "span-name" }); span.setName("updated-name").end(); ```
### Add Checkpoint To add a checkpoint use `setCheckpoint`.
Android ```kotlin val span: Span = Measure.startSpan("span-name").setCheckpoint("checkpoint-name") ```
iOS ```swift let span: Span = Measure.startSpan(name: "span-name").setCheckpoint("checkpoint-name") ```
Flutter ```dart final span = Measure.instance.startSpan("span-name").setCheckpoint("checkpoint-name"); ```
React Native ```typescript const span = Measure.startSpan({ name: "span-name" }).setCheckpoint("checkpoint-name"); ```
### Deferred Span Start The span builder API allows pre-configuring a span without starting it immediately.
Android ```kotlin val spanBuilder: SpanBuilder = Measure.createSpan("span-name") val span: Span = spanBuilder.startSpan() ```
iOS ```swift let spanBuilder: SpanBuilder = Measure.createSpanBuilder(name: "span-name")! let span: Span = spanBuilder.startSpan() ```
Flutter ```dart final spanBuilder = Measure.instance.createSpanBuilder("span-name"); final span = spanBuilder.startSpan(); ```
React Native ```typescript const spanBuilder = Measure.createSpanBuilder({ name: "span-name" }); const span = spanBuilder?.startSpan(); ```
## Distributed Tracing Distributed tracing is a monitoring method that helps tracking requests as they travel through different services in a distributed system (like microservices, serverless functions and mobile apps). The `traceparent` header is a key component of distributed tracing that helps track requests as they flow through different services. It follows the [W3C Trace Context specification](https://www.w3.org/TR/trace-context/#header-name) and consists of four parts in a single string: Example: `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01` Where: * `00`: Version * `4bf92f3577b34da6a3ce929d0e0e4736`: Globally unique trace ID * `00f067aa0ba902b7`: Parent span ID (representing the current operation) * `01`: Trace flags (like whether sampling is enabled) When your mobile app makes API calls, including this header allows you to correlate the client-side operations with server-side processing, giving you end-to-end visibility of your request flow. ### Get a trace parent header
Android ```kotlin val span = Measure.startSpan("http") val key = Measure.getTraceParentHeaderKey() val value = Measure.getTraceParentHeaderValue(span) ```
iOS ```swift let span = Measure.startSpan(name: "http") let key = Measure.getTraceParentHeaderKey() let value = Measure.getTraceParentHeaderValue(span: span) ```
Flutter ```dart final span = Measure.instance.startSpan("http"); final key = Measure.instance.getTraceParentHeaderKey(); final value = Measure.instance.getTraceParentHeaderValue(span); ```
React Native ```typescript const span = Measure.startSpan({ name: "http" }); const key = Measure.getTraceParentHeaderKey(); const value = Measure.getTraceParentHeaderValue({ span }); ```
## Sampling See [Configuration Options](https://measure.sh/docs/configuration-options) for details on how to configure sampling for performance traces. ## Recipes ### Distributed Tracing with OkHttp Interceptor ```kotlin // First, create an interceptor to handle tracing class TracingInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val span = Measure.startSpan("http") try { // Add traceparent header to the request val originalRequest = chain.request() val tracedRequest = originalRequest.newBuilder() .header( Measure.getTraceParentHeaderKey(), Measure.getTraceParentHeaderValue(span) ) .build() // Execute the request val response = chain.proceed(tracedRequest) // Set span status based on response if (response.isSuccessful) { span.setStatus(SpanStatus.Ok) } else { span.setStatus(SpanStatus.Error) } return response } finally { span.end() } } } // Set up OkHttp client with the interceptor val okHttpClient = OkHttpClient.Builder() .addInterceptor(TracingInterceptor()) .build() ``` ### Distributed Tracing with URLSession Interceptor ```swift // Create a URLSession interceptor to handle tracing class TracingInterceptor: NSObject, URLSessionTaskDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { let span = Measure.startSpan(name: "http") var tracedRequest = request tracedRequest.addValue( Measure.getTraceParentHeaderValue(span: span), forHTTPHeaderField: Measure.getTraceParentHeaderKey() ) completionHandler(tracedRequest) span.setStatus(.ok).end() } } // Set up URLSession with the interceptor let session = URLSession(configuration: .default, delegate: TracingInterceptor(), delegateQueue: nil) ``` h3: ### Distributed Tracing with Dio Interceptor ```dart class TraceHeaderInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { // Start a new span for the HTTP request final span = Measure.instance.startSpan("http"); // Get the trace parent header key and value final key = Measure.instance.getTraceParentHeaderKey(); final value = Measure.instance.getTraceParentHeaderValue(span); // Add the trace header to the request options.headers[key] = value; // Store the span in extra data so we can finish it later options.extra['trace_span'] = span; super.onRequest(options, handler); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { final span = response.requestOptions.extra['trace_span']; span?.end(); super.onResponse(response, handler); } @override void onError(DioException err, ErrorInterceptorHandler handler) { final span = err.requestOptions.extra['trace_span']; span?.end(); super.onError(err, handler); } } ``` ### Distributed Tracing with React Native fetch ```typescript import { Measure } from '@measuresh/react-native'; async function trackedFetch(url: string, options?: RequestInit) { const span = Measure.startSpan({ name: "http" }); const headers: Record = { ...(options?.headers as Record), [Measure.getTraceParentHeaderKey()]: Measure.getTraceParentHeaderValue({ span }), }; try { const response = await fetch(url, { ...options, headers }); span.end(); return response; } catch (error) { span.setAttribute("error", true).end(); throw error; } } ``` ## Screen Load Time Time taken to load an Activity/Fragment in Android or a UIViewController in iOS is automatically tracked using a trace. ### Android **Activity load time** measures the time between the Activity being created and the first frame drawn on the screen. This is also known as Time to First Frame or Time to Initial Display (TTID). Each Activity load time is captured using a unique span with the name `Activity TTID` followed by the fully qualified class name of the Activity. For example, for `MainActivity` the span name would be `Activity TTID com.example.MainActivity`. An attribute called `app_startup_first_activity` with value of _true_ is automatically added to the span to indicate if the Activity was being loaded as part of a [cold](https://measure.sh/docs/feature-app-launch-metrics) launch. **Fragment load time** measures the time between the Fragment view being created and the first frame drawn on the screen. This is also known as Time to First Frame (TTF) or Time to Initial Display (TTID). Each Fragment load time is captured using a unique span with the name `Fragment TTID` followed by the fully qualified class name of the Fragment. For example, for `HomeFragment` the span name would be `Fragment TTID com.example.HomeFragment`. A large TTID means users wait too long to see content while navigating the app. > Note that the fully qualified activity name may be truncated to fit within the 64 character limits for span names. #### How it works **Activity load time** a span starts in the `onCreate` callback of the Activity. The span ends when the first frame is drawn on the screen. This is achieved using two techniques: 1. Attach a `View.addOnAttachStateChangeListener` to the root view of the Activity to detect when the view is attached to the window. 2. Post a runnable using `Handler.sendMessageAtFrontOfQueue` on the main thread as a proxy for detecting when the first frame is drawn. #### Data Captured A span with the name `Activity TTID {fully qualified activity name}` is created for each Activity load. And a span with the name `Fragment TTID {fully qualified fragment name}` is created for each Fragment load. The Activity TTID span may have the following attributes if applicable: | 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. | #### Spans Visualized Using Perfetto The following image shows TTID spans of an Activity along with its two Fragments visualized using Perfetto. ![screen-load-span.png](../assets/android-screen-load-span.png) ### iOS **ViewController load time** measures the time between the ViewController's view being loaded and the first frame drawn on the screen. This is also known as Time to First Frame or Time to Initial Display (TTID). Each ViewController load time is captured using a unique span with the name `VC TTID` followed by the fully qualified class name of the ViewController. For example, for `MainViewController` the span name would be `VC TTID MainViewController`. An attribute called `app_startup_first_view_controller` with a value of _true_ is automatically added to the span to indicate if the ViewController was being loaded as part of a cold launch. A large TTID means users wait too long to see content while navigating the app. > Note that the fully qualified ViewController name may be truncated to fit within the 64 character limits for span names. #### How It Works **ViewController load time** tracking works by monitoring the ViewController's lifecycle events: 1. The span starts in either the `loadView` or `viewDidLoad` callback of the ViewController: - If you inherit from `MsrViewController` (Swift) or `MSRViewController` (Objective-C), the span will start from `loadView`. - Otherwise, the span will start from `viewDidLoad`. 2. The span ends when `viewDidAppear` is called, indicating that the view is fully visible on screen. The SDK uses method swizzling to automatically track these lifecycle events without requiring any manual instrumentation. #### Data Captured A span with the name `VC TTID {view controller name}` is created for each ViewController load. The ViewController TTID span may have the following attributes if applicable: | 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/features/feature-custom-events --- # Track Custom Events * [**Introduction**](#introduction) * [**Custom Events in Session Timeline**](#custom-events-in-session-timeline) * [**API Reference**](#api-reference) * [**Track a Custom Event**](#track-a-custom-event) * [**Custom Event Attributes**](#custom-event-attributes) * [**Custom Event with Timestamp**](#custom-event-with-timestamp) * [**React Native**](#react-native) ## Introduction Custom events let you add your own context on top of the automatically collected events. They’re great for tracking things that matter to your app, like user actions, feature usage, feature flags or any other domain-specific data. This helps you debug issues more effectively and analyze the real-world impact of your features. > [!TIP] > > Some events like Screen View and Handled errors/exceptions can be tracked using dedicated methods. This allows you to maintain a consistent structure for common events, making them easier to search and analyze. ## Custom Events in Session Timeline Custom events are included in the [Session Timeline](https://measure.sh/docs/feature-session-timelines) along with other automatically collected events. The event and its attributes can be viewed in the timeline, making it easy to get context on what happened during a session. ## API Reference ### Track a Custom Event To track a custom event, use the `trackEvent` method. #### Android ```kotlin Measure.trackEvent("event_name") ``` #### iOS Using Swift: ```swift Measure.trackEvent(name: "event_name") ``` Using ObjC: ```objc [Measure trackEvent:@"event_name"]; ``` #### Flutter ```dart Measure.instance.trackEvent(name: "event_name"); ``` ### Custom Event Attributes A custom event can also contain attributes, which are key-value pairs. > [!NOTE] > - Attribute keys must be strings with a maximum length of `256` characters. > - Attribute keys must only contain alphabets, numbers, hyphens and underscores. > - Attribute values must be one of the primitive types: `int`, `long`, `double`, `float` or `boolean`. > - String attribute values can have a maximum length of `256` characters. #### Android ```kotlin val attributes = AttributesBuilder() .put("is_premium_user", true) .build() Measure.trackEvent("event_name", attributes = attributes) ``` #### iOS Using Swift: ```swift Measure.trackEvent(name: "event_name", attributes: ["is_premium_user": .bool(true)]) ``` Using ObjC: ```objc [Measure trackEvent:@"event_name" attributes:@{@"is_premium_user": YES}]; ``` #### Flutter ```dart final attributes = AttributeBuilder() .add("is_premium_user", true) .build(); Measure.instance.trackEvent(name: "event_name", attributes: attributes); ``` ### Custom Event with Timestamp You can also record a custom event with a specific timestamp. This is useful for tracking events that happened before the SDK was initialized, like events during app startup. The timestamp should be in milliseconds since epoch. #### Android ```kotlin Measure.trackEvent("event_name", timestamp = 1734443973879L) ``` #### iOS Using Swift: ```swift Measure.trackEvent(name: "event_name", timestamp: 1734443973879) ``` Using ObjC: ```objc [Measure trackEvent:@"event_name" timestamp:1734443973879]; ``` #### Flutter ```dart Measure.instance.trackEvent(name: "event_name", timestamp: 1734443973879); ``` ## React Native To track a custom event: ```typescript import { Measure } from '@measuresh/react-native'; Measure.trackEvent({ name: "event_name" }); ``` To include attributes: ```typescript Measure.trackEvent({ name: "event_name", attributes: { is_premium_user: true, screen: "Home", retry_count: 2, }, }); ``` To record a custom event with a specific timestamp, pass the timestamp in milliseconds since epoch. Use `Measure.getCurrentTime()` to get an accurate timestamp from a monotonic clock. ```typescript const timestamp = Measure.getCurrentTime(); Measure.trackEvent({ name: "event_name", timestamp }); ``` --- Source: https://measure.sh/docs/features/feature-bug-report-android --- # Bug Reports — Android Bug reports enable users to report issues directly from the app. Measure SDK provides two approaches to implement bug reporting. * [Session Timeline](#session-timeline) * [Built-in Experience](#built-in-experience) * [Theming](#theming) * [Permissions](#permissions) * [Custom Experience](#custom-experience) * [Attachments](#attachments) * [Capture Screenshot](#capture-screenshot) * [Capture Layout Snapshot](#capture-layout-snapshot) * [Add Image from Gallery](#add-image-from-gallery) * [Limits](#limits) * [Add Attributes](#add-attributes) * [Shake to Report Bug](#shake-to-report-bug) * [Benchmarks](#benchmarks) ## Session Timeline When a bug report is captured, it automatically comes with a session timeline that includes all events that occurred 5 minutes before the bug report was submitted. This provides rich context to help diagnose and fix the reported issue. ## Built-in Experience Launch the default bug report interface using `Measure.launchBugReportActivity`. A screenshot is automatically taken when this method is called and added to the bug report. Users can choose to remove the screenshot if they wish. | Dark Mode | Light Mode | |--------------------------------------------------|----------------------------------------------------| | ![Dark Mode](../assets/android-bug-report-dark.png) | ![Light Mode](../assets/android-bug-report-light.png) | Usage: ```kotlin Measure.launchBugReportActivity() ``` To disable taking a screenshot when this method is called, pass in a parameter: ```kotlin Measure.launchBugReportActivity(takeScreenshot = false) ``` ### Theming You can apply a custom theme by overriding any of the values in the theme: [themes.xml](https://github.com/measure-sh/measure/tree/main/android/measure-android/measure/src/main/res/values/themes.xml). For more details on customizing themes, refer to the Android documentation on [theming](https://developer.android.com/develop/ui/views/theming/themes#CustomizeTheme). ### Permissions Users can add images from gallery to report bugs. We rely on the `androidx-activity` [photo picker](https://developer.android.com/training/data-storage/shared/photo-picker#device-availability). This requires **no additional permissions** to be declared in the app's manifest. The new photo picker UI is only available on Android 11 (API level 30) and above. For devices running Android 10 (API level 29) and below, this falls back to using `ACTION_OPEN_DOCUMENT` to select images from the gallery. Optionally, to achieve a consistent experience across all Android versions, you can follow the instructions in the official [documentation](https://developer.android.com/training/data-storage/shared/photo-picker#device-availability) to install a back-ported version of the photo picker. > [!NOTE] > If you are using Measure SDK version `0.11.0` or earlier, you'll need the `READ_MEDIA_IMAGES` permission > to allow users to select images from the gallery. Without this permission, the button to select images > from gallery will not be visible. ## Custom Experience You can build a custom experience to match the look and feel of your app. Once the bug report is entered by the user, trigger the `Measure.trackBugReport` method. ```kotlin // Track the bug report Measure.trackBugReport(description = "Items from cart disappear after reopening the app") ``` ### Attachments Bug reports can be enhanced with attachments. A maximum of `5` attachments can be added per bug report. You can add attachments to bug reports in the following ways: * [Capture Screenshot](#capture-screenshot) * [Capture Layout Snapshot](#capture-layout-snapshot) * [Add Image from Gallery](#add-image-from-gallery) #### Capture Screenshot Capture a screenshot using `captureScreenshot`. This function must be called from the main thread. ```kotlin private val attachments = mutableListOf() Measure.captureScreenshot(activity, onComplete = { attachment -> attachments.add(attachment) }) Measure.trackBugReport( description = "Items from cart disappear after reopening the app", attachments = attachments ) ``` > [!IMPORTANT] > For privacy, screenshots can be masked with the same configuration provided during SDK initialization. See all the configuration options [here](https://measure.sh/docs/configuration-options). #### Capture Layout Snapshot Capture a layout snapshot using `captureLayoutSnapshot`. This function must be called from the main thread. ```kotlin private val attachments = mutableListOf() Measure.captureLayoutSnapshot(activity, onComplete = { attachment -> attachments.add(attachment) }) Measure.trackBugReport( description = "Items from cart disappear after reopening the app", attachments = attachments ) ``` #### Add Image from Gallery To add images selected by a user from the gallery, use `imageUriToAttachment` to convert the `Uri` to an `Attachment`. Ensure the Uri has the `FLAG_GRANT_READ_URI_PERMISSION` permission granted, as shown in the example below. ```kotlin private val attachments = mutableListOf() registerForActivityResult( ActivityResultContracts.GetContent() ) { uri -> uri?.let { // Ensure the Uri has this permission to be able to read the image content context.contentResolver.takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION ) // This will compress the image and convert it into an attachment Measure.imageUriToAttachment(context, uri) { attachment -> attachments.add(attachment) } } } // Restrict picker to only allow selecting images .launch("image/*") Measure.trackBugReport( description = "Items from cart disappear after reopening the app", attachments = attachments ) ``` #### Limits - Each bug report can have a maximum of `5` attachments. - The bug report description can have a maximum length of `4000` characters. ## Add Attributes Attributes allow attaching additional contextual data to bug reports. This helps in adding relevant information about the user's state, app configuration or other metadata that can help with debugging. - Attribute keys must be strings with a maximum length of 256 characters. - Attribute values must be one of the primitive types: `int`, `long`, `double`, `float` or `boolean`. - String attribute values can have a maximum length of 256 characters. Add attributes when the Bug Report Activity is launched: ```kotlin val attributes = AttributesBuilder().put("is_premium", true).build() Measure.launchBugReportActivity(takeScreenshot = true, attributes = attributes) ``` or, when `trackBugReport` is called: ```kotlin val attributes = AttributesBuilder() .put("is_premium_user", true) .build() Measure.trackBugReport(description = "...", attributes = attributes) ``` ## Shake to Report Bug A shake listener can be set up to allow users to report bugs by shaking their device. This is particularly useful for quickly reporting issues without navigating through the app. To set up a shake listener, use the `setShakeListener` method. The listener will be triggered when a shake is detected, use the `launchBugReportActivity` method to open the bug report interface or implement a custom UI. > [!NOTE] > The listener can get called multiple times if the device is shaken multiple times in quick succession. > The `launchBugReportActivity` method handles this by ensuring that the bug report interface is only launched once. > However, if you implement a custom UI, you may need to handle this logic yourself. ```kotlin Measure.setShakeListener(object : MsrShakeListener { override fun onShake() { val attributes = AttributesBuilder().put("is_premium", true).build() Measure.launchBugReportActivity(takeScreenshot = true, attributes = attributes) } }) ``` To disable the shake listener, use: ```kotlin Measure.setShakeListener(null) ``` ## Benchmarks The bug reporting flow involves loading images from the file system or capturing screenshots, which might lead to increased memory and CPU consumption. The following Perfetto traces can help in debugging any potential bottlenecks: * `msr-captureScreenshot` — time spent on the main thread to capture and compress a screenshot. * `msr-loadImageFromFile` — time spent on the main thread to load an image from a file. * `msr-loadImageFromUri` — time spent on the main thread to load an image from a Uri. --- Source: https://measure.sh/docs/features/feature-bug-report-ios --- # Bug Reports — iOS Bug reports enable users to report issues directly from the app. There are two ways to use the bug reporting feature: 1. **Built-in Experience**: Use the default bug report interface provided by the SDK. 2. **Custom Experience**: Build a custom bug reporting UI and use the SDK to track bug reports programmatically. Additionally, the SDK supports using shake gestures to launch the bug reporting flow, allowing users to report bugs quickly and easily without having to navigate through the app. * [**Session Timeline**](#session-timeline) * [**Built-in Experience**](#built-in-experience) * [**Theming**](#theming) * [**Custom Experience**](#custom-experience) * [**Add Attributes**](#add-attributes) * [**Shake to Report Bug**](#shake-to-report-bug) ## Session Timeline When a bug report is captured, it automatically comes with a session timeline that includes all events that occurred 5 minutes before the bug report was submitted. This provides rich context to help diagnose and fix the reported issue. ## Built-in Experience Launch the default bug report interface using `Measure.launchBugReport`. A screenshot can be automatically taken and added to the bug report. Users can remove or add more attachments in the UI. | Dark Mode | Light Mode | |----------------------------------------------|------------------------------------------------| | ![Dark Mode](../assets/ios-bug-report-dark.png) | ![Light Mode](../assets/ios-bug-report-light.png) | When the screenshot button is clicked, a floating screenshot and exit button appear. Users can traverse through the app and take screenshots of the relevant screen. [Bug Report demo](https://github.com/user-attachments/assets/491e685b-e1ae-4c8d-ac36-8f42d73fa3eb) ### Example Usage ```swift Measure.launchBugReport(takeScreenshot: true) ``` To disable taking a screenshot when launching, set `takeScreenshot: false`: ```swift Measure.launchBugReport(takeScreenshot: false) ``` You can also pass a custom UI config and attributes: ```swift let color = BugReportConfig.default.colors.update(isDarkMode: false) let config = BugReportConfig(colors: color) let attributes: [String: AttributeValue] = ["user_id": .string("12345")] Measure.launchBugReport(takeScreenshot: true, bugReportConfig: config, attributes: attributes) ``` Currently, you can have a maximum of 5 attachments and a description length of 4000 characters. ### Theming You can customize the appearance of the bug report UI using `BugReportConfig`. To set the theme, update the colors using `.update(isDarkMode: ...)` and pass the result to the `BugReportConfig` initializer. Check out [BugReportConfig](../../ios/Sources/MeasureSDK/Swift/BugReport/BugReportConfig/) to see all the configurable tokens. ### Example Usage ```swift let color = BugReportConfig.default.colors.update(isDarkMode: false) let dimensions = MsrDimensions( topPadding: 20 ) let config = BugReportConfig(colors: color, dimensions: dimensions) Measure.launchBugReport(takeScreenshot: true, bugReportConfig: config) ``` ## Custom Experience You can build a custom bug reporting UI and use the SDK to track bug reports programmatically. ### Example Usage ```swift Measure.captureScreenshot(for: viewController) { screenshot in Measure.trackBugReport( description: "Items from cart disappear after reopening the app", attachments: [screenshot].compactMap { $0 }, attributes: ["is_premium": .bool(true)] ) } ``` ### Attachments Bug reports can include up to 5 attachments such as screenshots. ### Example Usage ```swift Measure.captureScreenshot(for: viewController) { screenshot in guard let screenshot else { return } Measure.trackBugReport(description: "Bug description", attachments: [screenshot], attributes: nil) } ``` ## Add Attributes You can attach additional contextual data to bug reports, such as user state or app configuration. ### Example Usage ```swift let attributes: [String: AttributeValue] = [ "is_premium_user": .bool(true), "user_id": .string("12345") ] Measure.launchBugReport(attributes: attributes) ``` Or with custom bug reports: ```swift Measure.trackBugReport(description: "Bug description", attachments: [], attributes: attributes) ``` ## Shake to Report Bug A shake listener can be set up to allow users to report bugs by shaking their device. This is particularly useful for quickly reporting issues without navigating through the app. To set up a shake listener, use the `onShake` method and pass a closure. This closure will be called whenever a shake gesture is detected. You can either launch the default bug report interface using `launchBugReport`, or implement your own custom UI. > [!NOTE] > The listener can get called multiple times if the device is shaken multiple times in quick succession. > The `launchBugReport` method handles this by ensuring that the bug report interface is only launched once. > However, if you implement a custom UI, you may need to handle this logic yourself. ```swift Measure.onShake { Measure.launchBugReport(takeScreenshot: true, bugReportConfig: BugReportConfig.default, attributes: nil) } ``` To disable the shake listener, use: ```swift Measure.onShake(nil) ``` --- Source: https://measure.sh/docs/features/feature-bug-report-flutter --- # Bug Reports — Flutter Bug reports enable users to report issues directly from the app. Measure SDK provides two approaches to implement bug reporting. * [**Session Timeline**](#session-timeline) * [Built-in Experience](#built-in-experience) * [Theming](#theming) * [I18n](#i18n) * [Custom Experience](#custom-experience) * [Attachments](#attachments) * [Limits](#limits) * [Add Attributes](#add-attributes) * [Shake to Report Bug](#shake-to-report-bug) * [Enabled/Disable shake detection](#enabledisable-shake-detection)] ## Session Timeline When a bug report is captured, it automatically comes with a session timeline that includes all events that occurred 5 minutes before the bug report was submitted. This provides rich context to help diagnose and fix the reported issue. ## Built-in Experience ### iOS | Dark Mode | Light Mode | |-------------------------------------------------------|---------------------------------------------------------| | ![Dark Mode](../assets/flutter-ios-bug-report-dark.jpeg) | ![Light Mode](../assets/flutter-ios-bug-report-light.jpeg) | ### Android | Dark Mode | Light Mode | |----------------------------------------------------------|------------------------------------------------------------| | ![Dark Mode](../assets/flutter-android-bug-report-dark.png) | ![Light Mode](../assets/flutter-android-bug-report-light.png) | Launch the default bug report interface by using `Measure.instance.createBugReportWidget` method. Usage: ```dart final widget = Measure.instance.createBugReportWidget(); ``` This is typically launched as a new screen: ```dart final navigatorState = Navigator.of(context); navigatorState.push( MaterialPageRoute( builder: (context) => Measure.instance.createBugReportWidget(), settings: const RouteSettings(name: '/msr_bug_report'), ), ); ``` ### Adding a screenshot Include a screenshot when opening the bug report widget. The screenshot is automatically redacted based on the [screenshot mask level](https://measure.sh/docs/configuration-options) configuration. ```dart final navigatorState = Navigator.of(context); final screenshot = await Measure.instance.captureScreenshot(); if (context.mounted) { navigatorState.push( MaterialPageRoute( builder: (context) => Measure.instance.createBugReportWidget(screenshot: screenshot), settings: const RouteSettings(name: '/msr_bug_report'), ), ); } ``` ### Theming Override the default primary color for the "Add from Gallery" and "Send" buttons: Example: ```dart Measure.instance.createBugReportWidget( theme: const BugReportTheme( colors: BugReportColors( primaryColor: Colors.lightBlue, ), ), ) ``` ### I18n Localize widget text using the BugReportTheme object: ``` Measure.instance.createBugReportWidget( theme: const BugReportTheme( text: BugReportText( appBarTitle: 'your-localized-text', sendButton: 'your-localized-text', inputPlaceHolder: 'your-localized-text', addFromGalleryButton: 'your-localized-text', ), ), ) ``` ## Custom Experience You can build a custom experience to match the look and feel of your app. Once the bug report is entered by the user, trigger the `Measure.trackBugReport` method. ```dart // Track the bug report Measure.instance.trackBugReport( description: "...", attachments: [], attributes: {}, ); ``` ### Attachments Bug reports can be enhanced with attachments. A maximum of `5` attachments can be added per bug report. You can add screenshots to bug reports by using the `Measure.captureScreenshot` method. ```dart final screenshot = await Measure.instance.captureScreenshot(); Measure.instance.trackBugReport( description: "...", attachments: [if (screenshot != null) screenshot], attributes: {}, ); ``` ### Limits - Each bug report can have a maximum of `5` attachments. - The bug report description can have a maximum length of `4000` characters. ## Add Attributes Attributes allow attaching additional contextual data to bug reports. This helps in adding relevant information about the user's state, app configuration or other metadata that can help with debugging. - Attribute keys must be strings with a maximum length of 256 characters. - Attribute values must be one of the primitive types: `int`, `long`, `double`, `float` or `boolean`. - String attribute values can have a maximum length of 256 characters. To add attributes, use the optinal `attributes` property in `createBugReportWidget`: ```dart final attributes = AttributeBuilder().add("order_id", "order-12345").build(); Measure.instance.createBugReportWidget( attributes: attributes, ); ``` Or add attributes with `trackBugReport` method. ```dart final attributes = AttributeBuilder().add("order_id", "order-12345").build(); Measure.instance.trackBugReport( description: "...", attachments: [], attributes: attributes, ); ``` ## Shake to Report Bug A shake listener can be set up to allow users to report bugs by shaking their device. This is particularly useful for quickly reporting issues without navigating through the app. Use `MsrShakeDetectorMixin` on top level widget in your app if you want to enable shake detection across the app. Setting up this mixin will automatically enable shake detection and provide a callback in `onShakeDetected` where you can launch the bug report interface. For example: ```dart class ShakeExample extends StatefulWidget { @override State createState() => _ShakeExampleState(); } class _ShakeExampleState extends State with MsrShakeDetectorMixin { @override void onShakeDetected() { _launchBugReport(); } void _launchBugReport() async { final screenshot = await Measure.instance.captureScreenshot(); Navigator.of(context).push( MaterialPageRoute( builder: (context) => Measure.instance.createBugReportWidget( screenshot: screenshot, ), ), ); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('Shake device to report bug'), ), ); } } ``` ### Enable/Disable shake detection To conditionally enable/disable shake detection call `disableShakeDetection` and `enableShakeDetection` methods available in the mixin. --- Source: https://measure.sh/docs/features/feature-screenshot-masking-swiftui --- # Screenshot Masking for SwiftUI Measure's screenshot masking works by traversing the UIKit view hierarchy to identify and redact sensitive views. Because SwiftUI renders its views differently from UIKit, the standard masking configuration options behave differently in SwiftUI contexts. To manage masking in SwiftUI, Measure provides the `.msrMask()` and `.msrUnmask()` view modifiers. # Table of Contents * [**How SwiftUI Masking Works**](#how-swiftui-masking-works) * [**Manual Masking**](#manual-masking) * [**`.msrMask()`**](#msrmask) * [**`.msrUnmask()`**](#msrunmask) * [**Examples**](#examples) * [**Masking a standalone Text view**](#masking-a-standalone-text-view) * [**Unmasking content inside a SwiftUI screen**](#unmasking-content-inside-a-swiftui-screen) * [**Mixed masking in a form**](#mixed-masking-in-a-form) # How SwiftUI Masking Works In UIKit, every visible element on screen corresponds to a `UIView` instance in the view hierarchy. Measure's masking traverses this hierarchy and redacts views that match known sensitive types such as `UILabel`, `UITextField` and `UITextView`. SwiftUI does not follow this pattern. Most SwiftUI views do not produce individual `UIView` instances. Instead, the entire SwiftUI view tree is hosted inside a `_UIHostingView`, which renders its content directly without creating UIKit counterparts for each view. As a result, **Measure masks the entire `_UIHostingView` frame by default** when a SwiftUI screen is present. This is the safest default — it ensures no SwiftUI content leaks into screenshots — but it means all SwiftUI content is masked, including content that may not be sensitive. Use `.msrUnmask()` to selectively expose content that is safe to show, and `.msrMask()` to explicitly mask content that needs to be hidden. # Manual Masking ## `.msrMask()` Marks a SwiftUI view as sensitive. The view's frame will be redacted in screenshots regardless of whether it would be detected automatically. Use this for: - Standalone `Text` views containing sensitive data outside of `List` contexts - Any custom SwiftUI view containing sensitive content that is not automatically detected ```swift Text(userEmail) .msrMask() ``` ## `.msrUnmask()` Exempts a SwiftUI view from masking. Use this to opt specific views out of the blanket `_UIHostingView` masking that is applied by default. When `.msrUnmask()` is applied to a view, its frame is subtracted from the masked region. This means content behind the unmasked view will also become visible in the screenshot. Use this for: - Non-sensitive SwiftUI content you want to remain visible in screenshots - Structural views like navigation titles, icons or static labels that contain no user data ```swift Text("Welcome back") .msrUnmask() ``` > [!NOTE] > `.msrUnmask()` takes precedence over automatic masking for the specific view it is applied to. > It does not affect sibling or parent views. # Examples ## Masking a standalone Text view By default, `Text` views outside a `List` are not automatically detected. Use `.msrMask()` to ensure they are redacted. ```swift var body: some View { VStack { Text("Account balance") // not sensitive, no modifier needed Text("\(user.balance)") // sensitive — mask explicitly .msrMask() } } ``` ## Unmasking content inside a SwiftUI screen Because all SwiftUI content is masked by default via `_UIHostingView`, use `.msrUnmask()` to expose non-sensitive content. ```swift var body: some View { VStack { Text("Hello, \(user.name)") // sensitive — remains masked by default Image(systemName: "star") // not sensitive — unmask explicitly .msrUnmask() } } ``` ## Mixed masking in a form In a form with both sensitive and non-sensitive fields, use both modifiers together. ```swift var body: some View { Form { Section("Profile") { Text("Username") // label — unmask .msrUnmask() TextField("Username", text: $username) // UITextField — masked automatically } Section("Security") { Text("Password") // label — unmask .msrUnmask() SecureField("Password", text: $password) // UITextField — masked automatically Text(sensitiveHint) // sensitive Text — mask explicitly .msrMask() } } } ``` > [!WARNING] > Do not use `.msrUnmask()` on views that contain or overlap sensitive content. The unmasked > frame is subtracted from the masked region, which means any sensitive content behind it will > also become visible in the screenshot. --- Source: https://measure.sh/docs/features/feature-screenshot-masking-flutter --- # Screenshot Masking for Flutter Measure captures screenshots on fatal erros, unhandled errors and bug reports. Before a screenshot leaves the device, sensitive content is redacted so it does not leak to the server. On Flutter, masking traverses the widget tree under the `MeasureWidget` and paints over the regions that match the configured [screenshot mask level](https://measure.sh/docs/configuration-options). # Table of Contents * [**How Flutter Masking Works**](#how-flutter-masking-works) * [**Mask Levels**](#mask-levels) * [**Manual Masking**](#manual-masking) * [**`MsrMask`**](#msrmask) * [**Examples**](#examples) # How Flutter Masking Works Masking walks the widget tree under the `MeasureWidget` that wraps your app and collects the regions to redact: - **Text** — `Text` and `RichText` - **Input fields** — `TextField` and `EditableText` - **Images** — `Image` Whether each region is masked depends on the active mask level. Two details are specific to Flutter: - **Clickable detection** is best-effort, based on the widget type. Text inside buttons, `InkWell`, or a `GestureDetector` with handlers is treated as clickable. - **Sensitive detection** treats a `TextField` as sensitive when `obscureText` is `true`, or when its `keyboardType` is `emailAddress`, `phone`, or `visiblePassword`. Sensitive fields are always masked, even at the *all text except clickable* level. # Mask Levels The mask level is configured remotely from the dashboard and applies to all platforms. See [Screenshot Mask Level](https://measure.sh/docs/configuration-options) for the full list of levels and what each one redacts. # Manual Masking ## `MsrMask` Widget Automatic detection covers standard text, input and image widgets. To always mask anything else, wrap it with `MsrMask`. The wrapped widget's area is always redacted, regardless of the configured mask level. ```dart MsrMask( child: AccountBalance(amount: balance), ) ``` --- Source: https://measure.sh/docs/features/feature-app-launch-metrics --- # App Launch Metrics * [**Introduction**](#introduction) * [**Sampling**](#sampling) * [**How It Works**](#how-it-works) * [**Android**](#android) * [**iOS**](#ios) ## Introduction Measure automatically tracks cold, warm and hot app launches along with the time taken for each. No sampling is done, ensuring that the data is accurate and represents the real-world performance of your app. - **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 easily track these metrics in the dashboard, allowing you to monitor the performance of your app's launch. ![App Launch Metrics](../assets/app-launch-metrics.png) ## Sampling See [Configuration Options](https://measure.sh/docs/configuration-options) for details on how to configure sampling for launch metrics. ## How It Works * [**Android**](#android) * [**iOS**](#ios) ### Android #### 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. > [!NOTE] > 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 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 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 * [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 #### Cold Launch 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 spanned. #### Warm Launch 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 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 * [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/features/feature-network-monitoring --- # Network Monitoring * [**Overview**](#overview) * [**Configuration Options**](#configuration-options) * [**Top endpoints**](#top-endpoints) * [**Request timeline**](#request-timeline) * [**Searching for endpoints**](#searching-for-endpoints) * [**How are endpoint patterns generated**](#how-are-endpoint-patterns-generated) Measure SDK can capture network requests, responses and failures along with useful metrics to help understand how APIs are performing in production from an end user perspective. ## Overview Most heavy lifting of network monitoring is done automatically by the Measure SDK. There are a lot of configuration options available to control the behavior of network monitoring. #### Android On Android, network requests made using the following clients, including any third party libraries that use them, are automatically tracked by simply adding the Measure Android Gradle Plugin: * [OkHttp](https://square.github.io/okhttp/): supported versions `4.7.0` to `5.3.2`. Requests made with versions outside this range will not be auto-instrumented. * [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection): requires minimum Android SDK version: 0.18.0. ##### Using Retrofit [Retrofit](https://square.github.io/retrofit/) uses OkHttp under the hood, but as a transitive dependency. Auto-instrumentation does not work with transitive dependencies, so projects that depend only on Retrofit are not instrumented automatically. Use one of the following workarounds: **Option 1: Declare OkHttp as a direct dependency** Pin the OkHttp version explicitly in your `build.gradle.kts`. This lets auto-instrumentation work without any code changes: ```kotlin dependencies { implementation("com.squareup.retrofit2:retrofit:3.0.0") implementation("com.squareup.okhttp3:okhttp:4.12.0") } ``` **Option 2: Add the interceptor manually** Configure the `OkHttpClient` passed to 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() ``` #### iOS On iOS, network requests made using the [URLSession](https://developer.apple.com/documentation/foundation/urlsession), including any third party libraries, are automatically tracked by simply adding the iOS SDK to your project. #### Flutter On Flutter, network requests made using the [Dio](https://pub.dev/packages/dio) package can be tracked by adding the `measure_dio` package to your project. This package provides `MsrInterceptor` that can automatically track network requests done using Dio. ```yaml dependencies: measure_dio: ^0.3.0 ``` ```dart final dio = Dio(); dio.interceptors.add(MsrInterceptor()); ``` For any other HTTP client libraries, you can manually track network requests using the `trackHttpEvent` method. Example using `http` package: ```dart import 'package:http/http.dart' as http; import 'package:measure/measure.dart'; Future trackedGet(Uri uri, {Map? headers}) async { return _trackRequest(() => http.get(uri, headers: headers), uri, 'get', headers); } Future trackedPost(Uri uri, {Map? headers, Object? body}) async { return _trackRequest(() => http.post(uri, headers: headers, body: body), uri, 'post', headers, body); } Future _trackRequest( Future Function() request, Uri uri, String method, Map? headers, [Object? body] ) async { final measure = Measure.instance; final startTime = measure.getCurrentTime(); try { final response = await request(); measure.trackHttpEvent( url: uri.toString(), method: method, statusCode: response.statusCode, startTime: startTime, endTime: measure.getCurrentTime(), requestHeaders: headers, responseHeaders: response.headers, requestBody: body?.toString(), responseBody: response.body, client: 'http', ); return response; } catch (e) { measure.trackHttpEvent( url: uri.toString(), method: method, startTime: startTime, endTime: measure.getCurrentTime(), failureReason: e.runtimeType.toString(), failureDescription: e.toString(), requestHeaders: headers, requestBody: body?.toString(), client: 'http', ); rethrow; } } ``` #### React Native On React Native, `fetch` and `XHR` requests use URLSession on iOS and OkHttp on Android under the hood. HTTP events are automatically tracked on iOS, but require additional setup on Android. ##### Android To track HTTP events on Android, configure a custom `OkHttpClient` with `OkHttpClientProvider` in `MainApplication.kt`: ```kotlin OkHttpClientProvider.setOkHttpClientFactory(object : OkHttpClientFactory { override fun createNewNetworkModuleClient(): OkHttpClient { return OkHttpClient.Builder() .cookieJar(ReactCookieJarContainer()) .addInterceptor(MeasureOkHttpApplicationInterceptor()) .eventListenerFactory(MeasureEventListenerFactory(null)) .build() } }) ``` Without this configuration, HTTP events are not tracked on Android. ##### iOS HTTP requests are tracked automatically on iOS. To also capture HTTP responses, add `MSRNetworkInterceptor` to the session configuration in `AppDelegate.mm`: ```objc RCTSetCustomNSURLSessionConfigurationProvider(^NSURLSessionConfiguration *{ NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; [MSRNetworkInterceptor enableOn:configuration]; return configuration; }); ``` ##### Manual tracking For HTTP clients not automatically instrumented, use `Measure.trackHttpEvent`: ```typescript import { Measure } from '@measuresh/react-native'; const startTime = Measure.getCurrentTime(); try { const response = await fetch('https://api.example.com/data'); await Measure.trackHttpEvent({ url: 'https://api.example.com/data', method: 'GET', startTime, endTime: Measure.getCurrentTime(), statusCode: response.status, }); } catch (error) { await Measure.trackHttpEvent({ url: 'https://api.example.com/data', method: 'GET', startTime, endTime: Measure.getCurrentTime(), error: (error as Error).message, }); } ``` ## Configuration Options See [Configuration Options - HTTP Events](https://measure.sh/docs/configuration-options) for all available options. ## Top endpoints The top endpoints section provides a ranked summary of your app's API endpoints across three dimensions: * **Slowest** - endpoints ranked by p95 latency, helping identify the slowest APIs affecting user experience. * **Highest Error %** - endpoints ranked by the percentage of 4xx and 5xx responses, highlighting unreliable APIs. * **Most Frequent** - endpoints ranked by total request count, showing which APIs are called most often. Clicking on any endpoint navigates to a detailed view with latency percentile charts (p50, p90, p95, p99) and status code distribution over time. These metrics are computed only for [endpoint patterns](#how-are-endpoint-patterns-generated) that have been automatically generated. New endpoints will appear here once enough traffic has been collected to generate a pattern. ![Top Endpoints](../assets/network-top-endpoints.png) ## Request timeline > This feature requires minimum SDK version: Android 0.16.2 and iOS 0.9.2 The request timeline is a heatmap that visualizes when network requests happen relative to session start. This helps answer questions like "which APIs fire immediately on app launch?" and "are there endpoints being called repeatedly in the background?" The timeline shows the top [endpoint patterns](#how-are-endpoint-patterns-generated) ranked by request frequency. New endpoints will appear here once enough traffic has been collected to generate a pattern. ![Request Timeline](../assets/network-request-timeline.png) ## Searching for endpoints You can search for any endpoint by typing its path in the "Explore endpoint" search bar. You can type the exact path or use wildcards to find multiple matching endpoints. #### Exact path Type the full path to find a specific endpoint: * `/v1/login` — matches only `/v1/login` * `/api/users/123/profile` — matches only `/api/users/123/profile` #### Using `*` to match any single part A `*` stands in for any single part of a path: * `/users/*/orders` — matches `/users/123/orders`, `/users/abc/orders`, etc. * `/api/*/profile/*` — matches `/api/users/profile/avatar`, `/api/teams/profile/settings`, etc. #### Using `**` to match everything after a prefix Place `**` at the end of a path to match everything that follows: * `/users/**` — matches `/users/123`, `/users/123/orders`, `/users/123/orders/456`, etc. * `/api/v1/**` — matches `/api/v1/login`, `/api/v1/users/123/profile`, etc. Note: `**` can only be used at the end of a path. #### Mixing `*` and `**` You can use `*` for specific parts and end with `**` to match the rest: * `/api/*/users/**` — matches `/api/v1/users/123`, `/api/v2/users/123/orders/456`, etc. ## How are endpoint patterns generated Measure automatically groups similar URLs into patterns so that requests like `/api/users/123/profile` and `/api/users/456/profile` are grouped together as `/api/users/*/profile`. A pattern is only created once it receives significant traffic - at least 100 requests in an hour. Once established, patterns are retained as long as they continue to receive traffic. Patterns are discovered every hour, and once available, metrics are refreshed every 15 minutes. Raw URLs are grouped into patterns in two stages: **1. Path normalization** - Each segment of a URL path is checked against known dynamic value formats. Segments that match any of the following are replaced with a `*` wildcard: * UUIDs (e.g. `550e8400-e29b-41d4-a716-446655440000`) * SHA1 and MD5 hashes * ISO 8601 date prefixes (e.g. `2024-01-15T...`) * Hex values (e.g. `0x1a2b3c`) * Integers with 2 or more digits (single-digit segments like `v1` are preserved) For example, `/api/users/550e8400-e29b-41d4-a716-446655440000/orders/12345` becomes `/api/users/*/orders/*`. **2. High-cardinality detection** - Some dynamic segments don't match a known format like UUIDs or integers. For these, Measure looks at the variety of values seen in each position of a path. If a segment has many distinct values (e.g. `/api/products/abc`, `/api/products/xyz`, ...), it is automatically replaced with `*` to form `/api/products/*`. --- Source: https://measure.sh/docs/features/feature-network-connectivity-changes --- # Network Connectivity Changes * [**Android**](#android) * [**iOS**](#ios)] * [**Flutter**](#flutter) Measure SDK captures changes to network connection state of the device. This includes changes to the type of network connection (WiFi, Mobile, etc.), the network provider (Airtel, T-Mobile, etc.) and the network generation (2G, 3G, 4G, etc.). ## Android > [!NOTE] > The SDK does not add any new permissions to the app. It only uses the permissions that the app already has. > Network connection state change feature is only enabled if the necessary permissions are already granted > to the app. Measure monitors changes in network. It is enabled only when the app is granted the ACCESS_NETWORK_STATE permission and the device is running on Android M (SDK 23) or a higher version. Measure does not add any new permissions to the app. It only uses the permissions that the app already has. Tracking of [network_generation] is limited to Cellular network for Android M (SDK 23) and later. This requires the app to hold the [READ_PHONE_STATE](https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE) permission, which is a runtime permission. In case the user denies this permission, network generation will not be captured. For devices running Android Tiramisu (SDK 33) or later, [READ_BASIC_PHONE_STATE](https://developer.android.com/reference/android/Manifest.permission#READ_BASIC_PHONE_STATE) permission is sufficient, and it does not require a runtime permission. ## iOS To detect network changes, Measure relies on NWPathMonitor's `pathUpdateHandler`. The `pathUpdateHandler` provides a callback whenever the network changes between cellular and Wi-Fi. Additionally, we perform independent checks to detect if the connection is accessed via VPN or if no internet is available. We rely on `CTTelephonyNetworkInfo` to get carrier information. > [!IMPORTANT] > Starting from iOS 16.4, access to the `network_provider` is no longer available and will be removed in a future > release of the SDK. ## Flutter Network changes are collected for Flutter apps based on the platform the app is running on (Android or iOS). The SDK uses the same methods as described above for Android and iOS. ## React Native Network changes are collected for React Native apps based on the platform the app is running on (Android or iOS). The SDK uses the same methods as described above for Android and iOS. --- Source: https://measure.sh/docs/features/feature-navigation-lifecycle-tracking --- # Navigation & Lifecycle Tracking - [**Introduction**](#introduction) - [**Lifecycle events**](#lifecycle-events) - [**Android**](#android) - [**Activity Lifecycle**](#activity-lifecycle) - [**Fragment Lifecycle**](#fragment-lifecycle) - [**AndroidX Navigation**](#androidx-navigation) - [**iOS**](#ios) - [**View Controller Lifecycle**](#view-controller-lifecycle) - [**SwiftUI lifecycle**](#swiftui-lifecycle) - [**Flutter**](#flutter) - [**React Native**](#react-native) - [**Manually track screen views**](#manually-track-screen-views) - [**Application foregrounded/backgrounded**](#application-foregroundedbackgrounded) ## Introduction When debugging your application, understanding how users navigate through your app and how different components of your app are initialized and destroyed is crucial. Measure provides automatic tracking of many lifecycle events in your application, allowing you to gain insights into user journeys and component lifecycles without writing additional code. There is also an option to manually track screen views if you need more control over what is tracked. For _Android_, Lifecycle events for Activity, Fragments and from AndroidX navigation library (including compose) are automatically tracked. For _iOS_, lifecycle events for View Controllers are automatically tracked. For `SwiftUI`, you can track lifecycle events by wrapping your views with `MsrMoniterView` or using the `moniterWithMsr` extension. Application foregrounded and backgrounded events are also automatically tracked, allowing you to understand when the application is visible to the user and when it is not. Apart from these automatically collected events, `trackScreenView` method can be used to manually track screen views in your application. This is useful for getting a more detailed view of user's journey depending on how your app is structured. ## Application foregrounded/backgrounded Measure automatically tracks when the application has come to foreground (is visible to the user) and when it has been put into background (is no longer visible to the user). ## Lifecycle events ### Android #### Activity Lifecycle Measure automatically tracks the following Activity lifecycle events: 1. [Created](https://developer.android.com/guide/components/activities/activity-lifecycle#oncreate) 2. [Resumed](https://developer.android.com/guide/components/activities/activity-lifecycle#onresume) 3. [Paused](https://developer.android.com/guide/components/activities/activity-lifecycle#onpause) 4. [Destroyed](https://developer.android.com/guide/components/activities/activity-lifecycle#ondestroy) #### Fragment Lifecycle Measure automatically tracks the following Fragment lifecycle events: 1. [Attached](https://developer.android.com/reference/androidx/fragment/app/Fragment.html#onAttach(android.content.Context)) 2. [Resumed](https://developer.android.com/reference/androidx/fragment/app/Fragment.html#onResume()) 3. [Paused](https://developer.android.com/reference/androidx/fragment/app/Fragment.html#onPause()) 4. [Detached](https://developer.android.com/reference/androidx/fragment/app/Fragment.html#onDetach()) #### AndroidX Navigation If you rely on AndroidX Navigation library for navigation in your Android app, a `screen_view` event is automatically tracked whenever a new destination is navigated to. This works using the instrumentation added by Measure Gradle Plugin and no code changes are needed to track these events. Supported `androidx.navigation:navigation-compose` versions: `2.4.0` to `2.9.7`. Apps using versions outside this range will not be auto-instrumented. ### iOS #### View Controller Lifecycle Measure automatically tracks the following View Controller lifecycle events: 1. `viewDidLoad` 2. `viewWillAppear` 3. `viewDidAppear` 4. `viewWillDisappear` 5. `viewDidDisappear` 6. `didReceiveMemoryWarning` 7. `initWithNibName` 8. `initWithCoder` > [!TIP] > > You can also track `loadView` and `deinit`/`dealloc` by inheriting from `MsrViewController` for swift > and `MSRViewController` for ObjC. An example of how to do this is shown below. Using Swift: ```swift class ViewController: MsrViewController { ... } ``` Using Objective-C: ```objc @interface ObjcDetailViewController: MSRViewController ... @end ``` #### SwiftUI lifecycle Measure can track SwiftUI component's `onAppear` and `onDisappear` if you wrap your view with the `MsrMoniterView`. It ensures that each appearance event is only triggered once per lifecycle instance. Additionally, you can use the `moniterWithMsr` extension to conveniently wrap any SwiftUI view. Example usage: **Using `MsrMoniterView`:** ```swift struct ContentView: View { var body: some View { MsrMoniterView("ContentView") { Text("Hello, World!") } } } ``` **Using `moniterWithMsr`:** ```swift struct ContentView: View { var body: some View { Text("Hello, World!") .moniterWithMsr("ContentView") } } ``` ### React Native The React Native SDK does not automatically track navigation events because React Native apps commonly use a variety of navigation libraries (e.g., React Navigation, Expo Router). Use `Measure.trackScreenView` to manually record screen view events when navigating between screens. For apps using [React Navigation](https://reactnavigation.org/), you can 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 }); } }} > {/* ... */} ); } ``` All underlying native Android and iOS lifecycle events (Activity, Fragment, UIViewController) are still automatically tracked for React Native apps. ## Manually track screen views If you want to manually track screen views in your application, you can use the `trackScreenView` method. This is useful when you want to track specific screens or views that are not automatically tracked by Measure. #### Android ```kotlin Measure.trackScreenView("Screen Name") ``` From version `0.12.0` onwards, you can also add attributes to the tracked screen view, which can be useful for providing additional context about the screen. - Attribute keys must be strings with a maximum length of 256 characters. - Attribute values must be one of the primitive types: `int`, `long`, `double`, `float` or `boolean`. - String attribute values can have a maximum length of 256 characters. ```kotlin Measure.trackScreenView( "TrackOrder", AttributesBuilder().put("order-id", "12345") .build() ) ``` #### iOS Using Swift: ```swift Measure.trackScreenView("Home") ``` Using ObjC: ```objc [Measure trackScreenView:@"Home"]; ``` #### Flutter To hook up with the Flutter navigation system, use the `MsrNavigatorObserver` which automatically tracks screen views when navigating between screens. You can add it to your `MaterialApp` or `CupertinoApp` as follows: ```dart @override Widget build(BuildContext context) { return MaterialApp( navigatorObservers: [MsrNavigatorObserver()], home: HomeScreen(), ); } ``` To manually track screen views in a Flutter application, you can use the `trackScreenViewEvent` method from the Measure SDK: ```dart Measure.instance.trackScreenViewEvent(name: "Home"); ``` > [!Note] > All Android/iOS lifecycle events are also automatically tracked for Flutter. #### React Native ```typescript import { Measure } from '@measuresh/react-native'; Measure.trackScreenView({ screenName: "Home" }); ``` You can also include attributes to provide additional context: ```typescript Measure.trackScreenView({ screenName: "TrackOrder", attributes: { order_id: "12345" } }); ``` --- Source: https://measure.sh/docs/features/feature-cpu-monitoring --- # CPU Monitoring - [**Android**](#android) - [**iOS**](#ios) - [**Flutter**](#flutter) Measure SDK captures CPU usage periodically (defaults to 3 seconds) when the app is in foreground. ## Android To calculate the CPU usage, two sets of information are required: 1. The CPU specification: The number of cores and the maximum frequency of each core. Number of cores are read from `Os.sysconf(OsConstants._SC_NPROCESSORS_CONF)` and the maximum frequency is read from `Os.sysconf(OsConstants._SC_CLK_TCK)`. 2. CPU time: The time spent by the CPU in executing instructions for an app. This information is read from `/proc/self/stat` file which is written to by the OS. The [/proc/self/stat](https://man7.org/linux/man-pages/man5/proc.5.html) file contains a number of metrics out of which we are interested only in the following: 1. utime - Amount of time that this process has been scheduled in user mode (this is where most of the application code runs), measured in clock ticks. 2. stime - Amount of time that this process has been scheduled in kernel mode (this is where most system processes run), measured in clock ticks. 3. cutime - Amount of time that this process's waited-for children have been scheduled in user mode, measured in clock ticks. 4. cstime - Amount of time that this process's waited-for children have been scheduled in kernel mode, measured in clock ticks. The average %CPU usage by the application in a given interval is calculated using: $\frac{{(utime - prev.utime) + (stime - prev.stime) + (cutime - prev.cutime) + (cstime - prev.cstime)}}{{clock\ speed \times interval \times number\ of\ cores}}$ Where: * `prev.utime`, `prev.stime`, `prev.cutime`, `prev.cstime` are values from the previous event collected. * `interval` is the time between two consecutive `cpu_usage` events. ## iOS Measure SDK calculates CPU usage by retrieving task and thread information using macOS/iOS system APIs. It performs the following steps: - Retrieve Task-Level CPU Usage: - Uses task_info with `TASK_BASIC_INFO` to get overall CPU time for the process. - This provides details such as total user time, system time and memory usage. - Retrieve Thread-Level CPU Usage: - Uses task_threads to obtain a list of active threads within the process. - Iterates through each thread and calls `thread_info` with `THREAD_BASIC_INFO`. - Extracts per-thread CPU usage statistics and aggregates them. - Calculate CPU Usage Percentage: - Each thread's CPU usage (cpu_usage field) is a fraction of `TH_USAGE_SCALE` (100% CPU time). - The SDK converts this into a percentage by summing the CPU usage of all non-idle threads and normalizing it against `TH_USAGE_SCALE`. This provides an estimate of how much CPU time the app is consuming relative to available CPU resources. ## Flutter CPU usage is collected for Flutter apps based on the platform the app is running on (Android or iOS). The SDK uses the same methods as described above for Android and iOS. ## React Native CPU usage is collected for React Native apps based on the platform the app is running on (Android or iOS). The SDK uses the same methods as described above for Android and iOS. #### Further reading * [Task Info](https://web.mit.edu/darwin/src/modules/xnu/osfmk/man/task_info.html) * [Thread Info](https://web.mit.edu/darwin/src/modules/xnu/osfmk/man/thread_info.html) * [Thread Basic Info](https://web.mit.edu/darwin/src/modules/xnu/osfmk/man/thread_basic_info.html) --- Source: https://measure.sh/docs/features/feature-memory-monitoring --- # Memory Monitoring - [Introduction](#introduction) - [Android](#android) - [iOS](#ios) - [Flutter](#flutter) ## Introduction Measure automatically tracks memory usage for each session. Memory usage is shown along with the session timeline and can be used to identify memory leaks or excessive memory usage in your app. ## Android A worker thread reads the following properties every 2 seconds and reports them to Measure: * **Max heap size** - The maximum amount of memory that the app can use. Collected using [Runtime.maxMemory](https://developer.android.com/reference/java/lang/Runtime#maxMemory()). * **Total heap size** - The total amount of memory that the app is currently using. Collected using [Runtime.totalMemory](https://developer.android.com/reference/java/lang/Runtime#totalMemory()). * **Free heap size** - The amount of memory that is currently free. Collected using [Runtime.freeMemory](https://developer.android.com/reference/java/lang/Runtime#freeMemory()). * **RSS (Resident set size)** - The amount of memory that the app is currently using, including the memory used by the native code. Collected by reading the [proc/pid/statm](https://man7.org/linux/man-pages/man5/proc.5.html) file. * **Total PSS (Proportional set size)** - The total amount of memory that the app is currently using, including the memory used by the native code, but excluding the memory used by the system. Collected using [Debug.getMemoryInfo](https://developer.android.com/reference/android/os/Debug#getMemoryInfo(android.os.Debug.MemoryInfo)). * **Native total heap size** - The total amount of memory that the app is currently using for native code. Collected using [Debug.getNativeHeapSize](https://developer.android.com/reference/android/os/Debug#getNativeHeapSize()). * **Native free heap size** - The amount of memory that is currently free for native code. Collected using [Debug.getNativeFreeHeapSize](https://developer.android.com/reference/android/os/Debug#getNativeHeapFreeSize()). ![Memory Usage](../assets/android-memory-usage.png) ## iOS Measure SDK calculates memory usage using the `task_info` API with the `TASK_VM_INFO` flavor. This API provides detailed memory statistics for the app’s process. The SDK extracts the `phys_footprint` value, which represents the physical memory footprint of the app. This value accounts for real memory pages in use, shared memory overhead and compressed memory, making it an accurate measure of the app’s actual memory consumption. If `phys_footprint` is unavailable, the SDK falls back to `resident_size`, which represents the resident memory size allocated to the process. However, `resident_size` does not reflect memory compression or pressure, so it may underreport actual memory usage compared to `phys_footprint`. SDK falls back to `resident_size` in below cases: - If `phys_footprint` is zero or not available due to system constraints. - If the `task_info` API call fails (rare, but possible due to system restrictions or sand-boxing). ![Memory usage](../assets/ios-memory-usage.png) #### Impact on Dashboard Metrics When `phys_footprint` is available, the reported memory usage reflects actual memory pressure. When `resident_size` is used, the reported value may be lower than real usage since it excludes compressed memory. The dashboard does not currently indicate when the SDK falls back to `resident_size`, but unusually low memory usage readings may suggest this fallback occurred. #### Further reading * [Task Info](https://web.mit.edu/darwin/src/modules/xnu/osfmk/man/task_info.html) * [iOS Memory Deep Dive](https://developer.apple.com/videos/play/wwdc2018/416/) * [Apple Developer Forum Thread](https://developer.apple.com/forums/thread/105088) ## Flutter Memory usage is collected for Flutter apps based on the platform the app is running on (Android or iOS). The SDK uses the same methods as described above for Android and iOS. ## React Native Memory usage is collected for React Native apps based on the platform the app is running on (Android or iOS). The SDK uses the same methods as described above for Android and iOS. --- Source: https://measure.sh/docs/features/feature-identify-users --- # Identify users Correlating sessions with users is critical for debugging certain issues. You can set a user ID which can then be used to query sessions on the dashboard. Note that the User ID is persisted across app launches automatically. > [!IMPORTANT] > > It is recommended to **avoid** the use of PII (Personally Identifiable Information) in the > user ID like email, phone number or any other sensitive information. Instead, use a hashed > or anonymized user ID to protect user privacy. ### Android To set a user ID. ```kotlin Measure.setUserId("user-id") ``` To clear a user ID. ```kotlin Measure.clearUserId() ``` ### iOS To set a user ID. ```swift Measure.setUserId("user-id") ``` To clear a user ID. ```swift Measure.clearUserId() ``` ### Flutter To set a user ID. ```dart Measure.instance.setUserId("user-id"); ``` To clear a user ID. ```dart Measure.instance.clearUserId(); ``` ### React Native To set a user ID. ```typescript import { Measure } from '@measuresh/react-native'; Measure.setUserId({ userId: "user-id" }); ``` To clear a user ID. ```typescript Measure.clearUserId(); ``` --- Source: https://measure.sh/docs/features/feature-manually-start-stop-sdk --- # Manually Start and Stop the SDK By default, initializing the SDK starts collection of events. To delay start to a different point in your app set `autoStart` to `false` during initialization. This can be used to control the scope of where the SDK is active in your application. > [!IMPORTANT] > Some SDK instrumentation remains active even when stopped. This is to maintain state and ensure seamless data > collection when it is started. > Additionally, cold, warm & hot launch events are also always captured. However, no data is sent to the server until > the SDK is started. ### Android ```kotlin Measure.init( context, MeasureConfig( // delay starting of collection autoStart = false, ) ) // Start collecting Measure.start() // Stop collecting Measure.stop() ``` ### iOS ```swift let config = BaseMeasureConfig(autoStart: false) // delay starting of collection let clientInfo = ClientInfo(apiKey: "", apiUrl: "") Measure.initialize(with: clientInfo, config: config) // Start collecting Measure.start() // Stop collecting Measure.stop() ``` ### Flutter ```dart Future main() async { await Measure.instance.init( () => runApp( MeasureWidget(child: MyApp()), ), config: const MeasureConfig( autoStart: false, // delay starting of collection ), clientInfo: ClientInfo( apiKey: "YOUR_API_KEY", apiUrl: "YOUR_API_URL", ), ); } // Start collecting Measure.instance.start(); // Stop collecting Measure.instance.stop(); ``` ### React Native ```typescript import { Measure, MeasureConfig } from '@measuresh/react-native'; await Measure.init({ config: new MeasureConfig({ autoStart: false, // delay starting of collection }) }); // Start collecting Measure.start(); // Stop collecting Measure.stop(); ``` --- Source: https://measure.sh/docs/features/feature-app-size-monitoring --- # App Size Monitoring * [**Android**](#android) * [**iOS**](#ios) Measure automatically calculates and tracks the size of your app over time. ## Android Measure supports tracking size for both APKs and AABs. The Measure Gradle Plugin automatically uploads the size information to Measure after an `assemble` or `bundle`task completes successfully. #### APK Size The plugin calculates APK size by using apkanalyzer [download-size](https://developer.android.com/tools/apkanalyzer#commands) command, which is the same tool used by Android Studio to calculate APK sizes. The APK size captured represents the estimated download size of the APK for the end user. #### AAB Size The plugin calculates AAB size by using bundletool's [get-size total](https://developer.android.com/tools/bundletool) command. The AAB size captured represents the maximum size of the APK that can be generated from the AAB. Read more about AAB format [here](https://developer.android.com/guide/app-bundle). ## iOS Measure automatically tracks size of the IPA file for each version when you upload dSYMs using the [upload_dsym_xcarchive.sh](../../ios/Scripts/upload_dsym_xcarchive.sh) or [upload_dsym_manual.sh](../../ios/Scripts/upload_dsym_manual.sh) script. > [!NOTE] > This represents the size of the generated .ipa, not the actual App Store download size. --- Source: https://measure.sh/docs/features/feature-alerts --- # Alerts Measure sends out alert notifications via email and slack on Crash spikes, ANR spikes and on receiving Bug Reports. Measure also sends out daily dummaries of core app metrics. * [**Email Integration**](#email-integration) * [**Setting Up Email**](#setting-up-email) * [**Slack Integration**](#slack-integration) * [**Connecting Slack Workspace**](#connecting-slack-workspace) * [**Receiving Alerts**](#receiving-alerts) * [**Stopping Alerts**](#stopping-alerts) * [**Disabling Slack Integration**](#disabling-slack-integration) ## Email Integration Email integration allows you to receive alert notifications and daily summaries in your inbox. ### Setting up email If you are a self hosted user, please set up your email integration if you haven't done so using this [guide](https://measure.sh/docs/../hosting/smtp-email). Once completed, all members in your team will receive emails for apps belonging to the same team. ## Slack Integration Slack integration allows you to receive alert notifications and daily summaries directly in your Slack workspace. ### Connecting Slack Workspace > [!NOTE] > > #### Self Hosted User > > If you are a self hosted user, please set up your slack integration if you haven't done so using this [guide](https://measure.sh/docs/../hosting/slack). Click `Add to Slack` button and authorize Measure Slack App for your workspace. ### Receiving Alerts To receive alert notifications in a channel, invite the Measure Slack app to the channel you wish to receive alerts in and then register it using the `/subscribe-alerts` slash command. ### Stopping Alerts To stop alert notifications in a channel, use the `/stop-alerts` slash command in a channel that is subscribed for receiving alerts. ### Listing Active Alert Channels To see all active channels that are subscribed for alerts, use the `/list-alert-channels` slash command in a channel that the Measure Slack app has been added to. ### Disabling Slack Integration You can stop all alerts regardless of channel subscription by disabling the Slack integration in the `teams` page on the Dashboard. --- Source: https://measure.sh/docs/features/feature-slack-integration --- # Slack Integration Measure's Slack integration lets you receive alert notifications and daily summaries directly in your Slack workspace. * [**Connecting Your Slack Workspace**](#connecting-your-slack-workspace) * [**Slash Commands**](#slash-commands) * [`/subscribe-alerts`](#subscribe-alerts) * [`/stop-alerts`](#stop-alerts) * [`/list-alert-channels`](#list-alert-channels) * [**Sending a Test Alert**](#sending-a-test-alert) * [**Disabling Slack Integration**](#disabling-slack-integration) * [**Self Hosted Setup**](https://measure.sh/docs/../hosting/slack) ## Connecting Your Slack Workspace > [!NOTE] > > #### Self Hosted Users > > If you are a self hosted user, please set up your Slack app if you haven't done so using this [guide](https://measure.sh/docs/../hosting/slack). Navigate to the **Team** settings page on the Measure dashboard and click the **Add to Slack** button. This will start an OAuth flow to authorize the Measure Slack app for your workspace. Once connected, you will see a toggle to enable or disable the integration and a **Send Test Alert** button to verify the connection is working. ## Slash Commands After connecting your workspace, invite the Measure bot to any channel where you want to receive alerts. Then use the following slash commands to manage alert subscriptions. ### `/subscribe-alerts` Registers the current channel to receive alert notifications. The Measure bot must be invited to the channel before running this command. ### `/stop-alerts` Unregisters the current channel from receiving alert notifications. Use this in a channel that is currently subscribed. ### `/list-alert-channels` Lists all channels in your workspace that are currently registered to receive alert notifications. Use this in any channel where the Measure bot has been added. ## Sending a Test Alert Once connected and enabled, you can verify your setup by clicking the **Send Test Alert** button on the **Team** settings page. This sends a test message to all subscribed channels. ## Disabling Slack Integration You can stop all alerts across every channel at once by disabling the Slack integration toggle on the **Team** settings page. Individual channel subscriptions are preserved and will resume if you re-enable the integration. --- Source: https://measure.sh/docs/features/feature-mcp --- # MCP Server 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) * [**Connecting to MCP via Coding Agents**](#connecting-to-mcp-via-coding-agents) * [**Available Tools**](#available-tools) * [`list_apps`](#list_apps) * [`get_filters`](#get_filters) * [`get_metrics`](#get_metrics) * [`get_app_health_over_time`](#get_app_health_over_time) * [`get_errors`](#get_errors) * [`get_error`](#get_error) * [`get_errors_over_time`](#get_errors_over_time) * [`get_error_over_time`](#get_error_over_time) * [`get_error_distribution`](#get_error_distribution) * [`get_error_common_path`](#get_error_common_path) * [`get_sessions`](#get_sessions) * [`get_sessions_over_time`](#get_sessions_over_time) * [`get_session`](#get_session) * [`get_bug_reports`](#get_bug_reports) * [`get_bug_reports_over_time`](#get_bug_reports_over_time) * [`get_bug_report`](#get_bug_report) * [`get_root_span_names`](#get_root_span_names) * [`get_span_instances`](#get_span_instances) * [`get_span_metrics_over_time`](#get_span_metrics_over_time) * [`get_trace`](#get_trace) * [`get_alerts`](#get_alerts) * [`get_journey`](#get_journey) * [`update_bug_report_status`](#update_bug_report_status) ## 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 You can connect your favorite coding agents to Measure as a remote MCP server. The MCP endpoint is available at: | Version | Endpoint | | ----------- | --------------------------------------------- | | Cloud | `https://api.measure.sh/mcp` | | Self Hosted | `https://[your-measure-api-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/) - [**Gemini CLI**](https://geminicli.com/docs/tools/mcp-server/) - [**XCode**](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) - [**Android Studio**](https://developer.android.com/studio/gemini/add-mcp-server) - [**VSCode**](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) - [**Cursor**](https://cursor.com/docs/context/mcp) - [**Windsurf**](https://docs.windsurf.com/windsurf/cascade/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 ### `list_apps` List all apps the authenticated user has access to. ### `get_filters` Get available filter options (versions, OS, countries, devices, etc.) for an app. ### `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 the app health timeline: sessions, crashes (fatal exceptions) and ANRs bucketed over time. ### `get_errors` Get error groups (crashes, non-fatal exceptions and ANRs) for an app, filterable by error type and severity. ### `get_error` Get individual error events (exception or ANR) for a specific error group. ### `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 time-series of occurrences for a specific error group. ### `get_error_distribution` Get attribute distribution (OS, device, version, country) for a specific error group. ### `get_error_common_path` Get the most common user navigation path leading to a specific error group. ### `get_sessions` Get sessions for an app, ordered by most recent first. ### `get_sessions_over_time` Get time-series of session counts. ### `get_session` Get full session with all events. ### `get_bug_reports` Get bug reports for an app, ordered by most recent first. ### `get_bug_reports_over_time` Get time-series of bug report counts. ### `get_bug_report` Get a single bug report with full details. ### `update_bug_report_status` Update the status of a bug report (open or closed). ### `get_root_span_names` Get all root span names for an app. ### `get_span_instances` Get span instances for a root span name. ### `get_span_metrics_over_time` Get p50/p90/p95/p99 duration metrics over time for a span name. ### `get_trace` Get full trace with all child spans. ### `get_alerts` Get alerts for an app, ordered by most recent first. ### `get_journey` Get user navigation journey graph with session counts between screens. --- Source: https://measure.sh/docs/features/configuration-options --- # Configuration Options Measure provides a number of configuration options to customize data collection and SDK behavior. These options are available in two ways: * **SDK Options** — Set at initialization time in your app's code. * **Remote Configuration Options** — Configured remotely from the Measure dashboard. Changes take effect without releasing a new app version. # Table of Contents * [**SDK Configuration Options**](#sdk-configuration-options) * [**API Reference**](#api-reference) * [**Android**](#android) * [**iOS**](#ios) * [**Flutter**](#flutter) * [**React Native**](#react-native) * [**trackActivityIntentData**](#trackactivityintentdata) * [**enableLogging**](#enablelogging) * [**autoStart**](#autostart) * [**requestHeadersProvider**](#requestheadersprovider) * [**maxDiskUsageInMb**](#maxdiskusageinmb) * [**enableFullCollectionMode**](#enablefullcollectionmode) * [**enableDiagnosticMode**](#enablediagnosticmode) * [**Remote Configuration Options**](#remote-configuration-options) * [**Crash Reporting**](#crash-reporting) * [**ANR Reporting**](#anr-reporting) * [**Trace Sampling**](#trace-sampling) * [**Launch Metrics Sampling**](#launch-metrics-sampling) * [**Journey Sampling**](#journey-sampling) * [**HTTP Events**](#http-events) * [**Screenshot Mask Level**](#screenshot-mask-level) # SDK Configuration Options ## API Reference #### Android For Android, options can be set in the `MeasureConfig` object which is passed to the `Measure.init` method. Example: ```kotlin Measure.init( context, MeasureConfig( enableLogging = true, autoStart = true, maxDiskUsageInMb = 50, trackActivityIntentData = true, requestHeadersProvider = customRequestHeadersProvider, enableFullCollectionMode = false, enableDiagnosticMode = false, ) ) ``` #### iOS For iOS, options can be set in the `BaseMeasureConfig` object which is passed to the `Measure.initialize` method. Example: ```swift let config = BaseMeasureConfig(enableLogging: true, autoStart: true, maxDiskUsageInMb: 50, requestHeadersProvider: customRequestHeadersProvider, enableFullCollectionMode: false) Measure.initialize(with: clientInfo, config: config) ``` #### Flutter For Flutter, options can be set in the `MeasureConfig` object which is passed to the `Measure.init` method. Example: ```dart Future main() async { await Measure.instance.init( () => runApp(MeasureWidget(child: MyApp())), config: const MeasureConfig( enableLogging: true, autoStart: true, enableDiagnosticMode: false, ), ); } ``` #### React Native For React Native, options can be set in the `MeasureConfig` object which is passed to the `Measure.init` method. Example: ```typescript import { Measure, MeasureConfig } from '@measuresh/react-native'; const config = new MeasureConfig({ enableLogging: true, autoStart: true, enableDiagnosticMode: false, }); await Measure.init({ config }); ``` ## `trackActivityIntentData` _Applies only to Android._ Android [Intent](https://developer.android.com/reference/android/content/Intent#standard-extra-data) can contain a bundle with any arbitrary information. While this can be useful to debug certain issues which require checking what data was passed as part of the bundle, it might also contain sensitive information. `trackActivityIntentData` allows enabling/disabling of collection of intent data for the following events: * `lifecycle_activity.created` event, which is collected with the Activity lifecycle event `onCreate` is triggered. * `cold_launch` event, which is collected when the app is launched from a cold start. * `warm_launch` event, which is collected when the app is launched from a warm start. * `hot_launch` event, which is collected when the app is launched from a hot start. Defaults to `false`. ## `enableLogging` Allows enabling/disabling internal logging of Measure SDK. This is useful to debug issues with the SDK itself. Defaults to `false`. ## `autoStart` Controls whether to start tracking immediately or delay starting the SDK. Defaults to `true`. Use `Measure.start` to start the SDK at a different point and `Measure.stop` to stop the SDK from tracking data. ## `requestHeadersProvider` Allows configuring custom HTTP headers for requests made by the Measure SDK to the Measure API. This is useful **only for self-hosted** clients who may require additional headers for requests in their infrastructure. Defaults to `null`, which means no additional headers are added. The following headers are reserved by the SDK and will be ignored if provided: - `Content-Type` - `msr-req-id` - `Authorization` - `Content-Length` #### Android Usage ```kotlin class CustomHeaderProvider : MsrRequestHeadersProvider { private val requestHeaders: ConcurrentMap = ConcurrentHashMap() fun addHeader(key: String, value: String) { requestHeaders[key] = value } fun removeHeader(key: String) { requestHeaders.remove(key) } override fun getRequestHeaders(): Map { return requestHeaders.toMap() // Return immutable copy } } Measure.init( context, MeasureConfig( requestHeadersProvider = CustomHeadersProvider() ) ) ``` #### iOS Usage Using Swift: ```swift class CustomHeaderProvider: NSObject, MsrRequestHeadersProvider { func getRequestHeaders() -> NSDictionary { return ["X-App-Version": "1.0.0"] } } Measure.initialize(with: clientInfo, config: BaseMeasureConfig(requestHeadersProvider: CustomHeadersProvider())) ```
Using ObjC ```objc @interface RequestHeaderProvider : NSObject @end @implementation RequestHeaderProvider - (NSDictionary *)getRequestHeaders { return @{ @"X-Custom-Header": @"value" }; } @end // add below snippet while initializing the SDK. ClientInfo *clientInfo = [[ClientInfo alloc] initWithApiKey:@"api-key" apiUrl:@"api-url"]; BaseMeasureConfig *config = [[BaseMeasureConfig alloc] initWithEnableLogging:YES autoStart:YES requestHeadersProvider:NULL maxDiskUsageInMb:nil enableFullCollectionMode:NO enableDiagnosticMode:NO enableDiagnosticModeGesture:NO]; [Measure initializeWith:clientInfo config:config]; ```
## `maxDiskUsageInMb` Allows setting the maximum disk usage for Measure SDK. This is useful to control the amount of disk space used by the SDK for storing session data, crash reports and other collected information. All Measure SDKs store data to disk and upload it to the server in batches. While the app is in foreground, the data is synced periodically and usually the disk space used by the SDK is low. However, if the device is offline or the server is unreachable, the SDK will continue to store data on disk until it reaches the maximum disk usage limit. Defaults to `50MB`. Allowed values are between `20MB` and `1500MB`. Any value outside this range will be clamped to the nearest limit. Note that the storage usage is not exact and works on estimates and typically the SDK will use much less disk space than the configured limit. When the SDK reaches the maximum disk usage limit, it will start deleting the oldest data to make space for new data. # `enableFullCollectionMode` Overrides all sampling configurations and enables full data collection for all sessions. Use this option for debugging and testing purposes. Defaults to `false`. Use this option with caution as it can lead to high data collection and storage costs if done at scale in production. ## `enableDiagnosticMode` Enables diagnostic mode, which writes all internal Measure SDK logs to files on disk. These files can be attached when reporting a bug to help us debug SDK issues. Defaults to `false`. > [!WARNING] > These files contain only Measure SDK logs, not your app's logs. Enable this option in debug builds > only. The log file location and the steps to retrieve them differ per platform. The [Enable Diagnostic Mode](https://measure.sh/docs/../sdk-integration-guide) section of the integration guide has the full step-by-step workflow. #### Android To pull all log files from the device to your machine: ```shell adb shell "run-as tar czf - files/measure/sdk_debug_logs/" > sdk_debug_logs.tar.gz ``` To delete all log files: ```shell adb shell run-as rm -rf files/measure/sdk_debug_logs/ ``` #### iOS Enable the `enableDiagnosticMode` and `enableDiagnosticModeGesture` options to `true` to enable a two-finger double tap gesture for sharing logs via the share sheet. ```swift let config = BaseMeasureConfig(enableDiagnosticMode: true, enableDiagnosticModeGesture: true) Measure.initialize(with: clientInfo, config: config) ``` # Remote Configuration Options A number of configuration options are available remotely from the Measure dashboard. Changes take effect without releasing a new app version. To change the remote configuration settings, navigate to the "Apps" tab in the Measure dashboard. The SDK requests this configuration and caches it locally. For a new configuration to take effect, it takes two app launches. First, the SDK fetches the new configuration and caches it locally. On the next app launch, the new configuration takes effect. #### Defaults The following defaults are set for each app: | Configuration | Default Value | |--------------------------------------|-------------------------------------------------------------------------------------| | Take screenshot on Crash | true | | Crash session timeline duration | 300 seconds (5 minutes) | | Take screenshot on ANR | true | | ANR session timeline duration | 300 seconds (5 minutes) | | Bug Report session timeline duration | 300 | | Trace sampling rate | 0.01% | | Journey sampling rate | 0.01% | | Launch Metrics sampling rate | 0.01% | | 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 | ## Crash Reporting ### Enable/Disable Screenshots Each Crash, by default comes with a screenshot of the app at the time of the crash. You can choose to disable this feature if you do not want screenshots to be captured. ### Session timeline duration When a crash occurs, a session timeline _up to a few minutes_ leading up to the crash is captured. You can configure this duration to control the number of events collected in each session timeline. By default, _5 minutes_ of events before the crash are captured in the session timeline. You can adjust this duration to control the amount of data collected. ## ANR Reporting ### Enable/Disable Screenshots Each ANR, by default comes with a screenshot of the app at the time of the ANR. You can choose to disable this feature if you do not want screenshots to be captured. ### Session timeline duration When an ANR occurs, a session timeline _up to a few minutes_ leading up to the ANR is captured. You can configure this duration to control the number of events collected in each session timeline. By default, _5 minutes_ of events before the ANR are captured in the session timeline. You can adjust this duration to control the amount of data collected. ## Bug Reports ### Session timeline duration When a bug report is submitted, a session timeline _up to a few minutes_ leading up to the bug report is captured. You can configure this duration to control the number of events collected in each session timeline. By default, _5 minutes_ of events before the bug report are captured in the session timeline. You can adjust this duration to control the amount of data collected. ## Trace Sampling By default, 0.01% (1 in 10000) traces are reported. You can adjust this percentage from 0.001% to 100% to control the amount of data collected. ## Launch Metrics Sampling Launch metrics include Cold Launch, Warm Launch and Hot Launch metrics shown on the Overview page on the dashboard. A sampling rate can be configured to control the number of sessions for whom these launch metrics are collected. By default, 0.01% (1 in 10000) of sessions will have launch metrics collected. You can adjust this percentage from 0.001% to 100% to control the amount of data collected. ## Journey Sampling Journey events are used to construct the Journey view in the Measure dashboard. Journey events include Activity Lifecycle events, Fragment Lifecycle events and Screen View events. A sampling rate can be configured to control the number of sessions for whom these Journey events are collected. By default, 0.01% (1 in 10000) of sessions will have Journey events collected. You can adjust this percentage from 0.001% to 100% to control the amount of data collected. ## HTTP Events HTTP events contain information about network requests made by your app. The following configuration options are available for HTTP events: ### HTTP Events Sampling > [!NOTE] > Required minimum SDK versions: Android 0.16.1 and iOS 0.9.2 A sampling rate can be configured to control how often HTTP events are collected. By default, 0.01% (1 in 10000) of HTTP events are collected. You can adjust this percentage from 0.001% to 100% to control the amount of data collected. ### Enable/Disable HTTP events You can choose to enable or disable the collection of HTTP events by providing a URL. We support both exact URL matches and wildcard URL matches (using `*` as a wildcard character). 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` ### Enable Request Body Collection By default, request body and headers are not collected for HTTP events. Note that enabling request body collection should only be done for very specific URLs which do not contain sensitive information and the payload size is not too large. You can provide a list of URLs with wildcards for which request body and headers should be collected. We support both exact URL matches and wildcard URL matches (using `*` as a wildcard character). ### Enable Response Body Collection By default, response body and headers are not collected for HTTP events. Note that enabling response body collection should only be done for very specific URLs which do not contain sensitive information and the payload size is not too large. You can provide a list of URLs with wildcards for which response body and headers should be collected. We support both exact URL matches and wildcard URL matches (using `*` as a wildcard character). ### Headers Blocklist Request and response headers are only collected if Request or Response Body Collection is enabled for a URL. By default, no headers are collected. This configuration allows you to specify which headers should be not collected for requests and responses. You can provide a list of header names to be blocked. Header name matching is case-insensitive. By default, the following headers are never collected, even if not specified in the blocklist: - Authorization - Cookie - Set-Cookie - Proxy-Authorization - WWW-Authenticate - X-Api-Key ## Screenshot Mask Level Change the masking level of screenshots collected with Crashes and ANRs. It helps prevent sensitive information from leaking. > [!NOTE] > _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. See [Screenshot Masking for SwiftUI](https://measure.sh/docs/feature-screenshot-masking-swiftui) > for details on how to control masking for SwiftUI views using the `.msrMask()` and `.msrUnmask()` > modifiers. The following levels of masking can be applied to the screenshots: #### Mask all text and media Masks all text, buttons, input fields, image views and video. Example: ![Mask All Text And Media](../assets/screenshot-mask-all-text-and-media.png) #### Mask all text Masks all text, buttons & input fields. Example: ![Mask All Text](../assets/screenshot-mask-all-text.png) #### Mask text except clickable Masks all text & input fields except clickable views like buttons. Example: ![Mask Text Except Clickable](../assets/screenshot-mask-text-except-clickable.png) #### Mask sensitive input fields Masks sensitive input fields like password, email & phone fields. Example: ![Mask Sensitive Input Fields](../assets/screenshot-mask-sensitive-input-fields-2.png) --- Source: https://measure.sh/docs/features/performance-impact --- # Performance Impact See the platform-specific sections below for details on the performance impact of the Measure SDK on your app. * [**Android**](#android) * [Benchmarks](#benchmarks) * [Profiling](#profiling) * [Comparison to Firebase initialization](#comparison-to-firebase-initialization) * [**iOS**](#ios) ## Android ### 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://measure.sh/docs/../../android/measure-android/benchmarks). > [!IMPORTANT] > 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 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 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. ![Firebase Comparison](../assets/android-firebase-bar-chart.png) Perfetto screenshot from one of the runs: ![Screenshot](../assets/android-firebase-comparison.png) ## iOS ## Benchmarks 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). > [!IMPORTANT] > 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) 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/sdk-upgrade-guides --- # SDK Upgrade Guides These docs contains detailed information about any breaking changes or behavior changes that you should be aware of when upgrading to a new version of the SDK. The guides are only created for versions that have non-trivial breaking changes or behavior changes. For minor releases that do not have any breaking changes or behavior changes, we recommend checking the release notes for more details. - [Android SDK v0.16.0](https://measure.sh/docs/android-v0.16.0) - [iOS SDK v0.9.0](https://measure.sh/docs/ios-v0.9.0) - [Measure Flutter SDK v0.4.0](https://measure.sh/docs/measure_flutter-v0.4.0) --- Source: https://measure.sh/docs/sdk-upgrade-guides/android-v0.16.0 --- # Upgrading to Android SDK v0.16.0 ## Breaking Changes #### **Changes to MeasureConfig** https://github.com/measure-sh/measure/pull/3069 The following properties from `MeasureConfig` have been removed and can be controlled directly from the dashboard from "Settings -> Apps -> Data Control" - `trackScreenshotOnCrash` - `screenshotMaskLevel` - `trackHttpBody` - `httpHeadersBlocklist` - `httpUrlBlocklist` - `httpUrlAllowlist` - `traceSamplingRate` - `coldLaunchSamplingRate` - `warmLaunchSamplingRate` - `hotLaunchSamplingRate` - `journeySamplingRate` `trackHttpHeaders` is completely removed. To control the tracking of HTTP headers, you can use the dashboard to enable/disable request or response body tracking which also controls the tracking of HTTP headers. `samplingRateForErrorFreeSessions` has been removed. A new flag called `enableFullCollectionMode` has been added to `MeasureConfig` which when enabled, will collect all events and spans by ignoring all sampling rates set on the dashboard. This can be used if you want to collect all events without sampling. Typically useful in debug builds. For more details, checkout the [configuration options](https://measure.sh/docs/../features/configuration-options) documentation. ## Behavior Changes #### **Improved Span validation** https://github.com/measure-sh/measure/pull/3081 Span name and attribute validations have been made stricter in accordance to the limits mentioned in the [documentation](https://measure.sh/docs/../features/feature-performance-tracing). If a span name exceeds the maximum allowed length, it will be discarded. While, if an attribute key name exceeds the maximum allowed length, the attribute will be discarded. A log message will be printed in both cases to help identify the issue during development. Earlier these would get discarded silently on the server with no feedback during development. #### **Session timeline duration** https://github.com/measure-sh/measure/pull/3069 For older versions, the SDK would report all events for a session when a crash, ANR or bug report was encountered. For very long sessions, this led to a lot of unnecessary events being reported which made it harder to get to the root cause. With this release, the SDK will only report events that occurred within a certain time window before the crash, ANR or bug report. This time window is configurable from the dashboard and can be set to a value can be configured in seconds to a max of 3600 seconds (1 hour). The default value is 300 seconds (5 minutes). #### **Addition of Baseline Profiles** https://github.com/measure-sh/measure/pull/3096 Baseline profiles are now shipped with the SDK which should help reduce the impact on app startup times. This should not lead to any issues; however, we suggest testing the app with R8 enabled to identify any issues during startup. --- Source: https://measure.sh/docs/sdk-upgrade-guides/ios-v0.9.0 --- # Upgrading to iOS SDK v0.9.0 ## Breaking Changes #### **Changes to MeasureConfig** https://github.com/measure-sh/measure/pull/3077 The following properties from `MeasureConfig` have been removed and can be controlled directly from the dashboard from "Settings -> Apps -> Data Control" - `trackScreenshotOnCrash` - `screenshotMaskLevel` - `trackHttpBody` - `httpHeadersBlocklist` - `httpUrlBlocklist` - `httpUrlAllowlist` - `traceSamplingRate` - `coldLaunchSamplingRate` - `warmLaunchSamplingRate` - `hotLaunchSamplingRate` - `journeySamplingRate` `trackHttpHeaders` is completely removed. To control the tracking of HTTP headers, you can use the dashboard to enable/disable request or response body tracking which also controls the tracking of HTTP headers. `samplingRateForErrorFreeSessions` has been removed. A new flag called `enableFullCollectionMode` has been added to `MeasureConfig` which when enabled, will collect all events and spans by ignoring all sampling rates set on the dashboard. This can be used if you want to collect all events without sampling. Typically useful in debug builds. For more details, checkout the [configuration options](https://measure.sh/docs/../features/configuration-options) documentation. ## Behavior Changes #### **Improved Span validation** https://github.com/measure-sh/measure/pull/3077 Span name and attribute validations have been made stricter in accordance to the limits mentioned in the [documentation](https://measure.sh/docs/../features/feature-performance-tracing). If a span name exceeds the maximum allowed length, it will be discarded. While, if an attribute key name exceeds the maximum allowed length, the attribute will be discarded. A log message will be printed in both cases to help identify the issue during development. Earlier these would get discarded silently on the server with no feedback during development. #### **Session timeline duration** https://github.com/measure-sh/measure/pull/3077 For older versions, the SDK would report all events for a session when a crash, ANR or bug report was encountered. For very long sessions, this led to a lot of unnecessary events being reported which made it harder to get to the root cause. With this release, the SDK will only report events that occurred within a certain time window before the crash, ANR or bug report. This time window is configurable from the dashboard and can be set to a value can be configured in seconds to a max of 3600 seconds (1 hour). The default value is 300 seconds (5 minutes). --- Source: https://measure.sh/docs/sdk-upgrade-guides/measure_flutter-v0.4.0 --- # Upgrading to Measure Flutter SDK v0.4.0 ## Breaking Changes #### **Changes to MeasureConfig** https://github.com/measure-sh/measure/pull/3090 The following properties from `MeasureConfig` have been removed and can be controlled directly from the dashboard from "Settings -> Apps -> Data Control" - `trackScreenshotOnCrash` - `trackHttpBody` - `httpHeadersBlocklist` - `httpUrlBlocklist` - `httpUrlAllowlist` - `traceSamplingRate` - `coldLaunchSamplingRate` - `warmLaunchSamplingRate` - `hotLaunchSamplingRate` - `journeySamplingRate` `trackHttpHeaders` is completely removed. To control the tracking of HTTP headers, you can use the dashboard to enable/disable request or response body tracking which also controls the tracking of HTTP headers. `samplingRateForErrorFreeSessions` has been removed. A new flag called `enableFullCollectionMode` has been added to `MeasureConfig` which when enabled, will collect all events and spans by ignoring all sampling rates set on the dashboard. This can be used if you want to collect all events without sampling. Typically useful in debug builds. `autoInitializeNativeSDK` has been removed. The native iOS and Android SDKs **must** be initialized manually. Read more about it in the [SDK initialization guide](https://measure.sh/docs/../sdk-integration-guide) documentation. For more details, checkout the [configuration options](https://measure.sh/docs/../features/configuration-options) documentation. ## Behavior Changes #### **Session timeline duration** https://github.com/measure-sh/measure/pull/3090 For older versions, the SDK would report all events for a session when a crash, ANR or bug report was encountered. For very long sessions, this led to a lot of unnecessary events being reported which made it harder to get to the root cause. With this release, the SDK will only report events that occurred within a certain time window before the crash, ANR or bug report. This time window is configurable from the dashboard and can be set to a value can be configured in seconds to a max of 3600 seconds (1 hour). The default value is 300 seconds (5 minutes). #### **Layout Snapshots** https://github.com/measure-sh/measure/pull/2751 [Layout Snapshots](https://measure.sh/docs/../features/feature-gesture-tracking) are now available for Flutter along with a new `measure_build` package that can be used to enhance the layout snapshots with widgets declared in your app. To learn more checkout the `measure_build` package [documentation](https://measure.sh/docs/../../flutter/packages/measure_build). --- Source: https://measure.sh/docs/hosting --- # Self Hosting Guide This guide helps you to self-host measure.sh on your own infrastructure. We recommend self hosting for small scale and hobby projects. Self hosting incorrectly at scale can lead to data loss, security issues and downtime. 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). ## Contents - [Objectives](#objectives) - [Prerequisites](#prerequisites) - [System Requirements](#system-requirements) - [Deploy on a Linux virtual machine](#deploy-on-a-linux-virtual-machine) - [1. SSH into your VM](#1-ssh-into-your-vm) - [2. Clone the measure repo](#2-clone-the-measure-repo) - [3. Run the `install.sh` script](#3-run-the-installsh-script) - [4. Configure and start your self hosted measure instance](#4-configure-and-start-your-self-hosted-measure-instance) - [5. Setup a reverse proxy server](#5-setup-a-reverse-proxy-server) - [6. Setup DNS A records](#6-setup-dns-a-records) - [7. Access your measure.sh dashboard](#7-access-your-measuresh-dashboard) - [Upgrade a Self Hosted Installation](#upgrade-a-self-hosted-installation) - [Run on macOS locally](#run-on-macos-locally) - [System Requirements](#system-requirements-1) - [1. Clone the measure repo](#1-clone-the-measure-repo) - [2. Run `config.sh` script to configure](#2-run-configsh-script-to-configure) - [3. Start the containers](#3-start-the-containers) - [4. Access your Measure dashboard](#4-access-your-measure-dashboard) - [Frequently Asked Questions](#frequently-asked-questions) - [Q. Can I use podman instead of docker?](#q-can-i-use-podman-instead-of-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) - [Q. How to perform healthcheck of Measure services?](#q-how-to-perform-healthcheck-of-measure-services) - [Q. Can I host Measure behind a VPN?](#q-can-i-host-measure-behind-a-vpn) - [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) - [Q. How to add or update environment variables?](#q-how-to-add-or-update-environment-variables) - [Q. How to setup complete symbolication for iOS?](#q-how-to-setup-complete-symbolication-for-ios) - [Q. Why does ClickHouse consume high amount of CPU or memory?](#q-why-does-clickhouse-consume-high-amount-of-cpu-or-memory) ## 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 - 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 - 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 Follow these step-by-step instructions to deploy measure.sh on a single Linux VM instance. ### 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 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. > [!IMPORTANT] > > 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 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 ``` > [!NOTE] > > 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 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.

Measure Configuration Wizard

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.

Measure Dashboard URL prompt

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/google-oauth) - [Create a GitHub OAuth App](https://measure.sh/docs/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/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/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. Once completed, the install script will attempt to start all the Measure docker compose services. You should see a similar output.

Successful installation

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 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 ``` > [!NOTE] > > 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 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 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 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.** > [!IMPORTANT] > > 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 ``` > [!NOTE] > > 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. > [!NOTE] > > 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 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. > [!WARNING] > > ### 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 > > 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 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 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. > [!IMPORTANT] > > 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 Run the `config.sh` script to auto configure most settings. ```sh ./config.sh ``` > [!NOTE] > > 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/google-oauth) - [Create a GitHub OAuth App](https://measure.sh/docs/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/smtp-email) Once your provider is set up, copy the values and enter them in the relevant prompts. ### 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 Visit [Dashboard](http://localhost:3000/auth/login) to access your dashboard and sign in to continue. ## Frequently Asked Questions Typical questions asked by other self host-ers. ### 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? 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? 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? 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? 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? 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? 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://measure.sh/docs/../../backend/symboloader) ### 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. > [!NOTE] > > If you want to discuss more, hop on to our [Discord](https://discord.gg/f6zGkBCt42) and ask your questions. #### 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 --- # Setup a Google OAuth Application 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). 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. Replace `yourcompany.com` with your domain. 17. Click **CREATE** 18. Copy the **Client ID** and the **Client Secret** [Go back to self host guide](https://measure.sh/docs) --- Source: https://measure.sh/docs/hosting/github-oauth --- # Setup a GitHub OAuth Application 1. Visit your GitHub organization's settings page, located at 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. 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. 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` [Go back to self host guide](https://measure.sh/docs) --- Source: https://measure.sh/docs/hosting/smtp-email --- # Set up an SMTP email provider 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 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 ``` [Go back to self host guide](https://measure.sh/docs) --- Source: https://measure.sh/docs/hosting/slack --- # Set up Slack integration Use this guide to setup Slack integration to receive Measure alert notifications on Slack. ## Contents - [Configure Slack settings for new installation](#configure-slack-settings-for-new-installation) - [Configure Slack settings for existing installation](#configure-slack-settings-for-existing-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: - **channels:read** - **chat:write** - **groups:read** - **commands** 5. **Slash Commands**. Go to `Slash Commands` section. Create the commands as follows: > [!NOTE] > > Please note that you need to use the **Measure API domain** in the below step 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 something like `https://[measure-api.yourcompany.com]/slack/events` in the below step. > > Replace **`[measure-api.yourcompany.com]`** with your actual Measure API domain. | Command | Request URL | Short Description | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | /subscribe-alerts | https://[measure-api.yourcompany.com]/slack/events

Replace **[measure-api.yourcompany.com]** with your actual Measure API domain. | Registers current channel to receive alert notifications | | /stop-alerts | https://[measure-api.yourcompany.com]/slack/events

Replace **[measure-api.yourcompany.com]** with your actual Measure API domain. | Stops current channel from receiving alert notifications | | /list-alert-channels | https://[measure-api.yourcompany.com]/slack/events

Replace **[measure-api.yourcompany.com]** with your actual Measure API domain. | Lists channels currently registered to receive alert notifications | ## Configure Slack settings for existing installation If you are upgrading from v0.8.2 or below, you would need to manually update the environment variables. After you've followed the above steps, instead of entering the client id and secrets in a terminal prompt, you will need to edit your environment variables file and restart the services. > [!NOTE] > > 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. 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 Salt**. Generate a random 44 character salt. Use the command `openssl rand -hex 22` to generate a random salt. ```sh SLACK_OAUTH_STATE_SALT=your-slack-oauth-state-salt # 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 ``` [Go back to self host guide](https://measure.sh/docs) --- Source: https://measure.sh/docs/hosting/migration-guides --- # Migration Guides 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). Some migrations may contain _optional_ steps. The specific guide would state that clearly. ## 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/v0.4.x) - Migration Guide for `v0.4.x` - [**v0.6.x**](https://measure.sh/docs/v0.6.x) - Migration Guide for `v0.6.x` - [**v0.8.x**](https://measure.sh/docs/v0.8.x) - Migration Guide for `v0.8.x` - [**v0.9.x**](https://measure.sh/docs/v0.9.x) - Migration Guide for `v0.9.x` - [**v0.10.x**](https://measure.sh/docs/v0.10.x) - Migration Guide for `v0.10.x` --- Source: https://measure.sh/docs/hosting/migration-guides/v0.4.x --- # Migration Guide for `v0.4.x` Use this guide only when you are on less than `v0.4.0` and upgrading to `v0.4.x`. > [!WARNING] > > Steps mentioned in this document will cause downtime. ## 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 ### 2. Bring down services > [!CAUTION] > 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 > [!CAUTION] > 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 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 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` Use this guide only when you are on less than `v0.6.0` and upgrading to `v0.6.x`. > [!WARNING] > > 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 ## 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 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` Use this guide only when you are on less than `v0.8.0` and upgrading to `v0.8.x`. > [!WARNING] > > 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 ## 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 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` Use this guide only when you are on less than `v0.9.0` and upgrading to `v0.9.x`. > [!WARNING] > > 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 ## 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 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 ```sh cd self-host ``` ```sh sudo ./config.sh --production --ensure ``` ## 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 ```sh sudo ./install.sh ``` --- Source: https://measure.sh/docs/hosting/migration-guides/v0.10.x --- # Migration Guide for `v0.10.x` Use this guide only when you are on less than `v0.10.0` and upgrading to `v0.10.x`. > [!WARNING] > > 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 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 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 > [!NOTE] > > 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 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 ```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. Should look like below: Image ## 6. Start Measure services ```sh sudo ./install.sh ``` ## 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 --- # 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 ## 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. ## Intelligent debugging, Seamless integration Connect Measure with your favorite coding agents through our [MCP](/product/mcp) server. Let your coding agent query errors, traces and session timelines directly in your development workflow. ## 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 or feature restrictions. - **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 Timeline](/product/session-timelines) 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 timelines | ✓ | Crash reports with limited breadcrumbs | | ANR detection with full session timelines | ✓ | ANRs with limited breadcrumbs | | Performance traces | ✓ | ✓ | | Network monitoring | ✓ | ✓ | | User journeys | ✓ | ✗ | | In-app bug reports | ✓ | ✗ | | Session timeline 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 --- # Looking for Firebase Crashlytics alternatives? Firebase Crashlytics is a free and popular crash reporting tool that many apps start with. Measure is a mobile first, open source Firebase Crashlytics alternative. ## Beyond Crashes Crashlytics handles basic crash reporting but requires more tooling to complete the mobile app monitoring picture. Performance traces need the performance monitoring add-on. Understanding what the user was doing when the crash happened requires enabling Google Analytics and manually instrumenting breadcrumb logs. Bug reporting requires third party tooling. Data analysis needs BigQuery export which is charged separately. The number of SDKs in your app and the tools you need to look at keep expanding. Measure unifies [Crashes & ANRs](/product/crashes-and-anrs), [Network Performance](/product/network-performance), [Performance Traces](/product/performance-traces), [App Health](/product/app-health), [Bug Reports](/product/bug-reports) and [User Journeys](/product/user-journeys) in one product. Every issue comes with a full Session Timeline which is auto collected for you without having to manually instrument every user interaction in your app. One SDK, one dashboard, one place to look so you can stop stitching context and get to the root cause faster. ## Full session context, not just stack traces Crash reports in Crashlytics come with stack traces and manually instrumented breadcrumbs. Taps, navigations, network calls or lifecycle events require manual instrumentation which needs to be updated and synced when your app code changes in new releases. Measure auto-captures gestures, navigation, lifecycle events, network calls and custom spans, and replays them as a [Session Timeline](/product/session-timelines) attached to every crash, ANR or error. Measure makes it easy to see what the user did, what the app did and where things went wrong, without instrumenting every screen by hand. ## Open source The Crashlytics SDKs are open source on GitHub, but the backend and dashboard are closed and run only on Google's proprietary infrastructure. 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, transparent pricing Crashlytics itself is free. The catch is that going further usually means stepping into the rest of the Firebase and GCP pricing ecosystem. BigQuery exports for data analysis, Cloud Functions for alerting and other paid GCP services require separate payment for advanced operations on your data. Measure has a single, transparent [price](/pricing) based on how much data you use. No per-seat charges, no hidden product bundles. With [Adaptive Capture](/product/adaptive-capture) you can dial collection up or down without rolling out app updates to control costs further. ## Built for mobile, by mobile devs Crashlytics sits inside the larger Firebase suite where mobile is one product line among many. The product roadmap and platform decisions compete with the priorities of a much bigger 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 how mobile apps actually break in production. Mobile is not a part of our product, it is the whole product. ## Measure vs Firebase Crashlytics | Capability | Measure | Firebase Crashlytics | | --- | --- | --- | | Crash reporting with full session timelines | ✓ | Crash reports with manual breadcrumbs | | ANR detection with full session timelines | ✓ | ANRs with manual breadcrumbs | | Performance traces without sampling | ✓ | Sampled | | Network monitoring without sampling | ✓ | Sampled | | User journeys | ✓ | Needs Google Analytics | | In-app bug reports | ✓ | ✗ | | Session timeline on every issue | ✓ | ✗ | | Dynamic Sampling with Adaptive Capture | ✓ | ✗ | | Auto-captured context | Gestures, navigation, network, lifecycle | Screen views if Google Analytics is enabled but rest needs manual instrumentation | | Pricing | Simple pricing based on data usage | Free crash reporting but complex Google Analytics + BigQuery pricing for advanced users | | Open Source | Apache 2.0 (OSI open source) | SDKs only | | Self-hostable | ✓ | ✗ | | Public roadmap & issue tracker | ✓ | SDK repos only | | Raw data access | Data export whenever you need it | Paid export to BigQuery only | | Mobile focus | ✓ | One of many Firebase products | 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 Timeline](/product/session-timelines) 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 timelines | ✓ | Crash reports with auto-captured events, session replay sampled & billed separately | | ANR detection with full session timelines | ✓ | ANRs, session replay sampled & billed separately | | Performance traces | ✓ | ✓ | | Network monitoring | ✓ | ✓ | | User journeys | ✓ | ✓ | | In-app bug reports | ✓ | ✗ | | Session timeline 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 Timeline](/product/session-timelines) 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 timelines | ✓ | ✓ | | ANR detection with full session timelines | ✓ | ✓ | | Performance traces | ✓ | ✓ | | Network monitoring | ✓ | ✓ | | User journeys | ✓ | ✓ | | In-app bug reports | ✓ | ✗ | | Session timeline 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 Timelines Every crash and ANR in your Android app arrives with a full [Session Timeline](/product/session-timelines). 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 timelines, 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 timeline. 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 timelines, 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're team Claude Code or Codex, or prefer open source agents and models, Measure fits right into your workflows. Works with Claude Code, Cursor, Codex, Gemini CLI, Windsurf, Cline, opencode and Kilo Code. 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 Timelines Every crash and error in your Flutter app comes with a complete [Session Timeline](/product/session-timelines) 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 timeline, 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 timeline. 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 timelines, 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're on Claude Code or Codex, or you prefer open source agents and models, Measure drops straight into your workflow. Works with Claude Code, Cursor, Codex, Gemini CLI, Windsurf, Cline, opencode and Kilo Code. 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 Timelines Every crash and error in your iOS app gets a complete [Session Timeline](/product/session-timelines) 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 timeline, 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 timeline. 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 timelines 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 lean on Claude Code or Codex, or prefer open source agents and models, Measure slots straight into your workflow. Works with Claude Code, Cursor, Codex, Gemini CLI, Windsurf, Cline, opencode and Kilo Code. 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 Timelines Every crash and error on iPad comes with a complete [Session Timeline](/product/session-timelines) 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 timeline 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 timeline, 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 timeline. 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 timelines, 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're on Claude Code or Codex, or you prefer open source agents and models, Measure drops straight into your workflow. Works with Claude Code, Cursor, Codex, Gemini CLI, Windsurf, Cline, opencode and Kilo Code. 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 Timelines Every error in your React Native app arrives with a complete [Session Timeline](/product/session-timelines). 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 timeline, 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 timeline. 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 timelines, 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 lean on Claude Code or Codex, or prefer open source agents and models, Measure fits right into your workflow. Works with Claude Code, Cursor, Codex, Gemini CLI, Windsurf, Cline, opencode and Kilo Code. 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 Timeline](/product/session-timelines) 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 timelines | ✓ | ✓ | | ANR detection with full session timelines | ✓ | ✓ | | Performance traces | ✓ | ✓ | | Network monitoring | ✓ | ✓ | | User journeys | ✓ | ✓ | | In-app bug reports | ✓ | ✓ | | Session timeline 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 Timeline](/product/session-timelines) 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 timelines | ✓ | Crash reports with optional Session replays | | ANR detection with full session timelines | ✓ | ANRs with optional Session replays | | Performance traces | ✓ | ✓ | | Network monitoring | ✓ | ✓ | | User journeys | ✓ | ✓ | | In-app bug reports | ✓ | ✗ | | Session timeline 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/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 timeline, 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 Connect Measure with your favorite coding agents through the Model Context Protocol. MCP lets AI coding assistants like Claude, Codex and Gemini access your errors, performance traces, session timelines and bug reports directly in your development workflow. With MCP, you can simply ask your AI assistant to look up an error, get the full context including stack traces and session timelines leading to the issue and harness the power of AI to ship fixes faster than ever before. 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 timeline 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-timelines --- # Session Timelines Debug issues faster by replaying the exact sequence of events that led to a crash or performance problem. Session Timeline 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 Timelines, you can stop guessing and have the full context you need to identify and fix root causes in an easy-to-navigate timeline. 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 Timeline](/product/session-timelines) 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 timelines | ✓ | Crash reports with breadcrumbs, Session replay billed separately | | ANR detection with full session timelines | ✓ | ANRs with breadcrumbs, Session replay billed separately | | Performance traces | ✓ | ✓ | | Network monitoring | ✓ | ✓ | | User journeys | ✓ | ✗ | | In-app bug reports | ✓ | ✓ | | Session timeline 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 Timelines](/product/session-timelines), 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: