# Track Metrics The SideKit SDK provides multiple ways to send analytics signals. This data powers your dashboard charts and allows you to build targeted version gates based on user behavior. Make sure you've configured the SDK with your API key before sending any signals to ensure they are tracked correctly. ## Add Your Custom Signal in the Dashboard Signals UI Navigate to the `Settings` page for your app and add your signal in the `Custom Signals` section. Here `purchases` is a custom signal we've chosen to track for this app. This whitelists the signal name and SideKit will accept data for this signal. ## Sending Signals :::ios You can track events in your app using three primary methods, depending on how much detail you need. ::: ### 1. Simple Key The simplest way to send a signal is with just a key string. This is perfect for binary events (like "session\_start"). :::ios ```swift SideKit.shared.sendSignal("user_login") ``` ::: ### 2. Key-Value Pairs Send a signal with both a key and a value. SideKit automatically generates pie charts in your dashboard for the distribution of these values. :::ios ```swift SideKit.shared.sendSignal(key: "purchase_amount", value: "99.99") ``` ::: :::ios ### 3. Signal Objects For more complex scenarios, you can use the `SideKit.Signal` struct. This is especially useful when building arrays of signals. ```swift SideKit.shared.sendSignal( SideKit.Signal(name: "button_click", value: "checkout") ) ``` ::: *** ## Bulk Tracking If you have multiple events that happen simultaneously, you can batch them into a single call: :::ios ```swift let signals = [ SideKit.Signal(name: "page_view", value: "home"), SideKit.Signal(name: "interaction", value: "header_clicked") ] SideKit.shared.sendSignal(signals) ``` ::: ## Best Practices
Batch when possible : If sending multiple signals in a loop, batch them into an array to reduce network requests. Use descriptive keys : Choose clear, consistent names (e.g., onboarding_step_1 ) to keep your dashboard organized. Analytics state : SideKit respects the user's analytics preference. If analytics is disabled, signals will be silently ignored.
# Feature Flags Feature flags let you turn features on or off from the SideKit dashboard, instantly, without submitting a new build for App Store review. Ship code behind a flag, then flip it live when you're ready — or kill a broken feature in seconds. A flag is a simple boolean: it's either ON or OFF for everyone using your app. For string or numeric values you want to tune remotely, use [Remote Config](/ios/feature-flags/remote-config) instead. ## Create a Flag in the Dashboard Open the **Feature Flags** panel for your app. Every flag is a key and an `ON`/`OFF` switch.
Type a key (for example multiplayer_enabled ) and press enter to create it. Flip the switch to set the flag ON or OFF . The toggle has a safety cover — click the cover to open it, then hit the switch to avoid accidental changes in production. Done tuning a flag? Archive it with the archive icon. Archived flags are still served to clients — their value is just locked, so old callers keep getting the last value you set.
Use clear, descriptive keys like evil\_difficulty or community\_goal\_enabled. The key you set here is exactly the string you'll read from the SDK. ## Read a Flag in Your App Flags are fetched automatically when you call [`configure`](/ios/getting-started/configure-sidekit) and cached on device. Read a flag anywhere with `SideKit.shared.flag(_:default:)`: ```swift if SideKit.shared.flag("multiplayer_enabled") { showMultiplayerButton() } ``` Always pass a `default` value that represents the "safe" fallback. It's returned when the flag doesn't exist yet, hasn't been fetched, or the device is offline with no cached value: ```swift let hardMode = SideKit.shared.flag("evil_difficulty", default: false) ``` The `default` parameter is optional — if you omit it, it's `false`. So `SideKit.shared.flag("evil_difficulty")` returns `false` for a missing, unfetched, or offline flag. Pass an explicit `default` whenever `false` isn't the safe fallback for that feature. flag(\_:default:) only returns boolean flags. If the key belongs to a Remote Config entry (a string value), the default is returned instead — use [`config(_:default:)`](/ios/feature-flags/remote-config) for those. ## Reacting to Flags in SwiftUI `SideKit.shared` is an `ObservableObject` and its `flags` array is `@Published`. Observe it to have your UI update automatically when flags refresh: ```swift struct ContentView: View { @ObservedObject private var sideKit = SideKit.shared var body: some View { VStack { if sideKit.flag("multiplayer_enabled") { MultiplayerLobby() } } } } ``` ## Refreshing Flags SideKit fetches the latest flags on launch (inside `configure`) and again whenever the app returns to the foreground. To pull the newest values on demand — for example after a specific user action — call `refreshFlags`: ```swift await SideKit.shared.refreshFlags() ``` If the network request fails, SideKit falls back to the last set of flags it cached on device, so your app keeps behaving predictably offline. What if there's no cache? If a fetch fails and nothing has been cached yet — a first launch with no connection, for example — SideKit simply keeps an empty flag set. It never throws, crashes, or blocks your UI. Every flag(\_:default:) call just returns the default you passed, so your app behaves exactly as if the flag were off (or whatever fallback you chose). ## How Flags Behave Wrap risky launches behind a flag that defaults to false. Ship the code dark, verify everything works, then flip the flag ON from the dashboard — and flip it back OFF instantly if something breaks. # Remote Config Remote Config delivers string values to your app at runtime. Unlike [feature flags](/ios/feature-flags/feature-flags) — which are on/off booleans — config entries hold text you can change on the fly: a goal number, a leaderboard payload, a promo message, or a JSON blob your app parses itself. Use a flag when the answer is yes/no (is this feature enabled?). Use config when you need an actual value (what's the target score?, who's on the leaderboard?). ## Add a Config Value in the Dashboard Open the **Remote Config** panel for your app. Each entry is a key paired with a string value that's delivered to clients at runtime.
Type a key (for example game_played_goal ), enter its value, and save. Click any value to edit it inline. Changes are delivered on the next fetch — no rebuild needed. Archive an entry with the archive icon to lock its value. Archived entries are still served to clients — old callers keep getting the last value you set — you just can't change it anymore.
Values are always stored as strings. For structured data — like top\_player\_history or top\_player\_runners\_up in the example above — store a JSON string and parse it on the client. ## Read a Config Value in Your App Config entries are fetched automatically during [`configure`](/ios/getting-started/configure-sidekit) and cached on device. Read one with `SideKit.shared.config(_:default:)`: ```swift let goal = SideKit.shared.config("game_played_goal", default: "1000") ``` The `default` is returned whenever the key doesn't exist, hasn't been fetched yet, or the device is offline with no cached value — so always provide a sensible fallback. The `default` parameter is optional — if you omit it, it's an empty string (`""`). So `SideKit.shared.config("game_played_goal")` returns `""` for a missing, unfetched, or offline key. Because an empty string is rarely useful, pass an explicit `default` you can actually fall back on. ### Converting Values Because every value is a string, convert to the type you need at the call site: ```swift // Numbers let goalCount = Int(SideKit.shared.config("game_played_goal", default: "1000")) ?? 1000 let streak = Int(SideKit.shared.config("goal_streak_val", default: "0")) ?? 0 ``` ### Parsing JSON Config For structured values, decode the string with `JSONDecoder`: ```swift struct TopPlayer: Codable { let name: String let count: Int } let raw = SideKit.shared.config("top_player_runners_up", default: "[]") let runnersUp = (try? JSONDecoder().decode( [TopPlayer].self, from: Data(raw.utf8) )) ?? [] ``` config(\_:default:) only returns Remote Config entries. If a key belongs to a boolean feature flag, the default is returned instead — read those with [`flag(_:default:)`](/ios/feature-flags/feature-flags). ## Reacting to Config in SwiftUI `SideKit.shared` is an `ObservableObject`, so a view that reads config re-renders when values refresh: ```swift struct GoalBanner: View { @ObservedObject private var sideKit = SideKit.shared var body: some View { Text("Goal: \(sideKit.config("game_played_goal", default: "1000"))") } } ``` ## Refreshing Config Config and flags share one system, so [`refreshFlags`](/ios/feature-flags/feature-flags) pulls the latest of both: ```swift await SideKit.shared.refreshFlags() ``` SideKit refreshes automatically on launch and when the app returns to the foreground, and falls back to the last cached values when the network is unavailable. What if there's no cache? If a fetch fails and nothing has been cached yet — a first launch with no connection, for example — SideKit simply keeps an empty config set. It never throws, crashes, or blocks your UI. Every config(\_:default:) call just returns the default you passed, so your app always has a value to work with. ## How Config Behaves # Add an App
Select Add new app from the app switcher in the top left of the dashboard.
Enter your app's name as well as an App Store and/or Play Store URL if your app is currently live. If your app is already live on the App Store, SideKit will automatically pull your app icon from your store URL. If it's not live yet, don't worry—you can always update the icon URL later.
## What's a Signal? A signal is an analytics metric that you'd like to track with SideKit. Any time the event you'd like to track takes place in your app you can [record it with the SideKit SDK](../analytics/track-a-signal). ### Default Signals SideKit automatically records three essential signals out-of-the-box so you can start seeing data immediately: We keep default signals to a minimum, you get to decide what you want to track. # Configure SideKit ## Add SideKit to your Project :::ios To add the SideKit iOS SDK to your Xcode project, follow these steps:
In Xcode, go to File \> Add Package Dependencies...
In the search bar, enter the GitHub URL:
``` https://github.com/appsidekit/ios-sdk ```
Select the ios-sdk package from the results
Configure the dependency rule:
  • Set Dependency Rule to "Up to Next Major Version"
  • This will use versions 1.0.0 up to (but not including) 2.0.0
Click Add Package to complete the installation.
Ensure SideKit appears in your project settings under General \> Frameworks, Libraries, and Embedded Content .
::: ## Configure SideKit in your app :::ios In your `ContentView`, add the configuration in a `.task` modifier with your [API key](/ios/helpers/find-api-key): ```swift .task { await SideKit.shared.configure( apiKey: "YOUR-API-KEY", verbose: true // Optional: enables detailed logs ) } ``` In your `AppDelegate` or `SceneDelegate`, call configure in `application(_:didFinishLaunchingWithOptions:)` or `scene(_:willConnectTo:options:)`: ```swift Task { await SideKit.shared.configure( apiKey: "YOUR-API-KEY", verbose: true // Optional: enables detailed logs ) } ``` ::: Set verbose = true during development to see all SideKit logs in your console. This is helpful for verifying that your signals are being tracked successfully. The SideKit SDK will now be available in your app, and you can start sending signals! # Find Your API Key Your API key is a unique identifier used to authenticate your app's requests to SideKit. ## Locating your API Key
Navigate to your app in the SideKit dashboard.
Select Settings from the sidebar navigation menu.
Scroll down to the Danger Zone section to find your key.
## Rotating Your API Key If your API key is compromised, you can generate a new one by clicking the **Rotate** button. Rotating your API key is irreversible. Once rotated:
  • Older app versions using the old key will stop sending signals.
  • Version gates will no longer be enforced on those versions.
  • You must release an update with the new key immediately.
# Find Your Bundle ID :::ios A **Bundle ID** (Bundle Identifier) is a unique identifier that represents your app on the App Store. They typically follow a reverse-DNS format, such as `com.yourcompany.appname`. Read more [here](https://developer.apple.com/documentation/appstoreconnectapi/bundle-ids). ## Locating the Bundle ID in Xcode You can find your Bundle ID in your project settings.
Open your project in Xcode and select the project file at the top of the Project Navigator .
Select your app target under the Targets list.
Go to the General tab and look for the Identity section.
::: Bundle IDs are case-sensitive! Double-check for any accidental capital letters or spaces. # Collect Feedback SideKit gives you a complete in-app feedback loop: send a report with one line of code, and it flows into a shared inbox in the dashboard where it's automatically grouped, triaged, and answerable. Make sure you've [configured the SDK](/ios/getting-started/configure-sidekit) with your API key before sending feedback, so reports are attributed to the right app. ## Send Feedback from Your App Call `sendFeedback` with the user's message. That's the only required argument — SideKit collects the device metadata for you: ```swift SideKit.shared.sendFeedback("The timer keeps resetting when I background the app.") ``` Hook it up to a text field and a submit button, and you have a working feedback form: ```swift struct FeedbackView: View { @State private var text = "" var body: some View { VStack { TextEditor(text: $text) Button("Send") { SideKit.shared.sendFeedback(text) text = "" } } } } ``` Feedback sends fire-and-forget — there's no need to await. Clear your input and show a thank-you the moment the user taps send. ## What Gets Collected Automatically You only pass the message. SideKit attaches the context you need to reproduce and triage a report: This metadata shows up next to every report in the dashboard arrives tagged with `iOS`, `v3.7.2`, `Version 26.5.1 (Build 23F81)`, `PL`, and `iPhone` without you writing any extra code. No begging users for context. ## Attributing Feedback to a User You can also attach arbitrary key-value context about the user with `userAttributes`: ```swift SideKit.shared.sendFeedback( "Crash on the stats screen.", endUserId: "user_12345", userAttributes: [ "plan": "pro", "signup_date": "2026-01-15" ] ) ``` A recipient must have an endUserId for you to reply to their feedback. Anonymous reports still land in your inbox — you just can't message back. ## Triage in the Dashboard Every report lands in a shared inbox. SideKit organizes it so you can act instead of sort.
Auto-grouping : SideKit clusters related reports into themes like "Leaderboard Issues" or "UI customization and haptics" so duplicates collapse into one actionable group. Hit Auto-group to file the ungrouped backlog. Labels : Each report and group is tagged as a Bug or Feature request, so you can filter to exactly what you're working on. Status : Move items through Open , In Progress , Resolved , and Won't Do to track what's handled. Reply : Respond to a single report or reply to everyone in a group at once. Replies reach any user who came in with an endUserId .
Reply to a whole group in one action to close the loop with every user who reported the same issue — a great way to tell people a fix has shipped. # Track Metrics The SideKit SDK provides multiple ways to send analytics signals. This data powers your dashboard charts and allows you to build targeted version gates based on user behavior. Make sure you've configured the SDK with your API key before sending any signals to ensure they are tracked correctly. ## Add Your Custom Signal in the Dashboard Signals UI Navigate to the `Settings` page for your app and add your signal in the `Custom Signals` section. Here `purchases` is a custom signal we've chosen to track for this app. This whitelists the signal name and SideKit will accept data for this signal. ## Sending Signals :::react-native You can track events in your app using two primary methods, depending on how much detail you need. ::: ### 1. Simple Key The simplest way to send a signal is with just a key string. This is perfect for binary events (like "session\_start"). :::react-native ```ts const { sendSignal } = useSideKit(); sendSignal("user_login") ``` ::: ### 2. Key-Value Pairs Send a signal with both a key and a value. SideKit automatically generates pie charts in your dashboard for the distribution of these values. :::react-native ```ts sendSignal("purchase_amount", "99.99") ``` ::: *** ## Bulk Tracking If you have multiple events that happen simultaneously, you can batch them into a single call: :::react-native ```ts const { sendSignals } = useSideKit(); sendSignals([ { key: "page_view", value: "home" }, { key: "interaction", value: "header_clicked" } ]) ``` ::: ## Best Practices
Batch when possible : If sending multiple signals in a loop, batch them into an array to reduce network requests. Use descriptive keys : Choose clear, consistent names (e.g., onboarding_step_1 ) to keep your dashboard organized. Analytics state : SideKit respects the user's analytics preference. If analytics is disabled, signals will be silently ignored.
# Add an App
Select Add new app from the app switcher in the top left of the dashboard.
Enter your app's name as well as an App Store and/or Play Store URL if your app is currently live. If your app is already live on the App Store, SideKit will automatically pull your app icon from your store URL. If it's not live yet, don't worry—you can always update the icon URL later.
## What's a Signal? A signal is an analytics metric that you'd like to track with SideKit. Any time the event you'd like to track takes place in your app you can [record it with the SideKit SDK](../analytics/track-a-signal). ### Default Signals SideKit automatically records three essential signals out-of-the-box so you can start seeing data immediately: We keep default signals to a minimum, you get to decide what you want to track. # Configure SideKit ## Add SideKit to your Project :::react-native To add the SideKit React Native SDK to your project, just install the NPM package: ```sh npm install @sidekit/react-native @react-native-async-storage/async-storage # or yarn add @sidekit/react-native @react-native-async-storage/async-storage ``` ::: ## Configure SideKit in your app :::react-native To initialize SideKit, wrap your app's contents with `SideKitProvider`: ```ts import { SideKitProvider } from '@sidekit/react-native'; function App() { return ( ); } ``` ::: Set verbose = true during development to see all SideKit logs in your console. This is helpful for verifying that your signals are being tracked successfully. The SideKit SDK will now be available in your app, and you can start sending signals! # Find Your API Key Your API key is a unique identifier used to authenticate your app's requests to SideKit. ## Locating your API Key
Navigate to your app in the SideKit dashboard.
Select Settings from the sidebar navigation menu.
Scroll down to the Danger Zone section to find your key.
## Rotating Your API Key If your API key is compromised, you can generate a new one by clicking the **Rotate** button. Rotating your API key is irreversible. Once rotated:
  • Older app versions using the old key will stop sending signals.
  • Version gates will no longer be enforced on those versions.
  • You must release an update with the new key immediately.
# Find Your Bundle ID :::react-native A **Bundle ID** (Bundle Identifier) is a unique identifier that represents your app on the App Store/Play Store. They typically follow a reverse-DNS format, such as `com.yourcompany.appname`. Read more [here](https://developer.apple.com/documentation/appstoreconnectapi/bundle-ids). ## Locating the Bundle ID in your Expo Project You can find your Bundle IDs for both iOS and Android in your app's `app.json` file: ```json { "expo": { ... "ios": { "bundleIdentifier": "com.sidekit.example" }, "android": { "package": "com.sidekit.example" }, ... } } ``` ::: Bundle IDs are case-sensitive! Double-check for any accidental capital letters or spaces. # Block an App Version ## In the Dashboard Manage Versions UI Navigate to `Versions` in the Dashboard under your app. SideKit will use analytics data to automatically infer the most popular app versions from the last 7 days. Tap configure to add an app version to SideKit's tracking. On an existing version you can set the status as forced or dismissible to block the version. Forced gates require users to update their app to proceed whereas dismissible gates can be skipped. ## In Your App ### Automatic ✨

Zero Code Required

SideKit automatically presents a beautiful version gate when users need to update. No setup, no code, no maintenance required.
Latest version pulled from App Store automatically "What's New" text synced from your App Store submission Works out-of-the-box with zero configuration
### Custom View for Blocked App Versions For custom version gates, see [Showing Custom Version Gates](./showing-custom-version-gates). # Set a Minimum Version ## In the Dashboard Manage Versions UI Navigate to `Versions` in the Dashboard under your app, set `MIN` from the dropdown to set an app version as a floor. Users on that app version or above will be able to access the app and any users below it will be shown a forced version gate which will require them to update their app to proceed. ## In Your App ### Automatic ✨

Zero Code Required

SideKit automatically presents a beautiful version gate when users need to update. No setup, no code, no maintenance required.
Latest version pulled from App Store automatically "What's New" text synced from your App Store submission Works out-of-the-box with zero configuration
Version gates triggered by minimum version requirements are never dismissible. Users must update to continue using the app. The "Skip for now" button only appears in dismissible version gates. ### Custom View for Blocked App Versions For custom version gates, see [Showing Custom Version Gates](./showing-custom-version-gates). # Showing Custom Version Gates ## Setup :::react-native To use custom version gates, configure SideKit with `presentationMode: "manual"`: ```ts import { SideKitProvider } from '@sidekit/react-native'; function App() { return ( ); } ``` ::: When using manual presentation mode, you're responsible for displaying the version gate UI. SideKit will still fetch and manage version requirements, but won't automatically show the update screen. ## GateInformation Struct Use the `GateInformation` struct to access version gate data and build your custom UI: :::react-native ```ts class GateInformation { gateType: VersionGateType; lastGateUpdate: string; latestVersion: string | null; whatsNew: string | null; storeUrl: string | null; } ``` ::: :::react-native ::: ## Gate Types The `VersionGateType` enum defines version gate behavior: :::react-native ```ts enum VersionGateType { Live = -1, Forced = 0, Dismissible = 1, } ``` ::: ### Understanding Gate Types
Live
The user's app version is not blocked by a version gate. You should not show an update screen in this case.
Forced
User must update to continue. The gate cannot be dismissed. Used for critical updates or minimum version enforcement.
Dismissible
User can dismiss the gate and continue using the app. Your custom view should include a close/skip button.
If the gate type is dismissible, your custom view must include UI to close or skip the update. Failing to do so will trap users in the update screen. ## Displaying Your Custom Gate :::react-native ```ts const { showUpdateScreen, gateInformation } = useSideKit(); {showUpdateScreen && gateInformation && ( {/* Show your custom update UI */} )} ``` ::: :::react-native Access gateInformation to get the current gate data, including version requirements, release notes, and App Store/Play Store URL. ::: # Block an App Version ## In the Dashboard Manage Versions UI Navigate to `Versions` in the Dashboard under your app. SideKit will use analytics data to automatically infer the most popular app versions from the last 7 days. Tap configure to add an app version to SideKit's tracking. On an existing version you can set the status as forced or dismissible to block the version. Forced gates require users to update their app to proceed whereas dismissible gates can be skipped. ## In Your App ### Automatic ✨

Zero Code Required

SideKit automatically presents a beautiful version gate when users need to update. No setup, no code, no maintenance required.
Latest version pulled from App Store automatically "What's New" text synced from your App Store submission Works out-of-the-box with zero configuration
### Custom View for Blocked App Versions For custom version gates, see [Showing Custom Version Gates](./showing-custom-version-gates). # Set a Minimum Version ## In the Dashboard Manage Versions UI Navigate to `Versions` in the Dashboard under your app, set `MIN` from the dropdown to set an app version as a floor. Users on that app version or above will be able to access the app and any users below it will be shown a forced version gate which will require them to update their app to proceed. ## In Your App ### Automatic ✨

Zero Code Required

SideKit automatically presents a beautiful version gate when users need to update. No setup, no code, no maintenance required.
Latest version pulled from App Store automatically "What's New" text synced from your App Store submission Works out-of-the-box with zero configuration
Version gates triggered by minimum version requirements are never dismissible. Users must update to continue using the app. The "Skip for now" button only appears in dismissible version gates. ### Custom View for Blocked App Versions For custom version gates, see [Showing Custom Version Gates](./showing-custom-version-gates). # Showing Custom Version Gates ## Setup :::ios To use custom version gates, configure SideKit with `presentationMode: .manual`: ```swift SideKit.shared.configure(apiKey: "YOUR-API-KEY", presentationMode: .manual) ``` ::: When using manual presentation mode, you're responsible for displaying the version gate UI. SideKit will still fetch and manage version requirements, but won't automatically show the update screen. ## GateInformation Struct Use the `GateInformation` struct to access version gate data and build your custom UI: :::ios ```swift struct GateInformation { let gateType: VersionGateType let lastGateUpdate: String let latestVersion: String? let whatsNew: String? let storeURL: String? } ``` ::: :::ios ::: ## Gate Types The `VersionGateType` enum defines version gate behavior: :::ios ```swift enum VersionGateType: Int, Codable { case live = -1 case forced = 0 case dismissible = 1 } ``` ::: ### Understanding Gate Types
Live
The user's app version is not blocked by a version gate. You should not show an update screen in this case.
Forced
User must update to continue. The gate cannot be dismissed. Used for critical updates or minimum version enforcement.
Dismissible
User can dismiss the gate and continue using the app. Your custom view should include a close/skip button.
If the gate type is dismissible, your custom view must include UI to close or skip the update. Failing to do so will trap users in the update screen. ## Displaying Your Custom Gate :::ios Bind your custom gate view to `SideKit.shared.showUpdateScreen` to control when it appears: ```swift struct ContentView: View { var body: some View { YourAppContent() .fullScreenCover(isPresented: SideKit.shared.$showUpdateScreen) { if let gateInfo = SideKit.shared.gateInformation { CustomVersionGateView(gateInfo: gateInfo) } } } } ``` ```swift class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Observe showUpdateScreen changes SideKit.shared.$showUpdateScreen .sink { [weak self] shouldShow in if shouldShow, let gateInfo = SideKit.shared.gateInformation { self?.presentCustomGate(gateInfo) } } .store(in: &cancellables) } func presentCustomGate(_ gateInfo: GateInformation) { let gateVC = CustomVersionGateViewController(gateInfo: gateInfo) gateVC.modalPresentationStyle = .fullScreen present(gateVC, animated: true) } } ``` ::: :::ios Access SideKit.shared.gateInformation to get the current gate data, including version requirements, release notes, and App Store URL. This data is automatically synced from your App Store listing. :::