Skip to content
The mobile SDKs are available on request. Email support@grona.ai to get access.

Test and debug

This page shows what the SDK is doing on a test device. The GronaDebug product adds a screen that lists the active configuration, every campaign and why it is live or not, bound slots and recent events. A logger routes SDK warnings to Xcode’s console. Both are meant for debug builds only.

  1. Add the GronaDebug product to your app target, as on Install the SDK.

  2. Import it and call it only inside #if DEBUG, from a hidden menu or a developer setting:

    DeveloperMenuViewController.swift
    #if DEBUG
    import Grona
    import GronaDebug
    import UIKit
    final class DeveloperMenuViewController: UIViewController {
    @objc func showGronaDebugView() {
    Grona.presentDebugView(from: self)
    }
    }
    #endif
  3. Tap Refresh on the debug screen after you change something in the app.

Swift Package Manager cannot link GronaDebug in Debug builds only, so two guards keep the debug view out of the app you ship:

  • Your #if DEBUG guards. With the import and every call inside #if DEBUG, your release build contains no code that opens the view. Xcode’s app templates set the DEBUG condition in the Debug configuration only.
  • GronaDebug’s own check. GronaDebug draws its view only when it was itself compiled with the DEBUG condition. Otherwise presentDebugView(from:) does nothing and logs a warning, and GronaDebugView is empty.

The debug view has no URL route, so nothing outside your own code can open it.

Section Contents
SDK SDK version, workspace, configuration source (bundled, cache or remote), configuration version, key id, expiry, last configuration error, consent state, the last 4 characters of the install id, whether a shopper is identified, and whether demo mode is in effect
Campaigns Each campaign’s name, id and placement, whether it would render now for this shopper, and if not, why (for example activation off, holdout, no grant or audience: profile unknown). Also the assigned arm and variant
Pins Local pins, with a field to pin a variant on this device and a button to clear pins
Slots Every bound slot and decision with its placement, screen and key, and the campaign it shows
Recent events Up to 200 recent events, newest first, with identifiers shortened

Local pins work only in debug builds and never create a grant. The recent-events list is kept only when the SDK is compiled with DEBUG, so release builds keep no copy of order ids, totals or product ids for it.

GronaLogger receives SDK diagnostics. Without one the SDK is silent. Send them to the unified logging system in debug builds:

AcmeGronaLogger.swift
import Grona
import OSLog
struct AcmeGronaLogger: GronaLogger {
func log(_ level: GronaLogLevel, _ message: String) {
let logger = Logger(subsystem: "com.example.acmemarket", category: "Grona")
switch level {
case .debug: logger.debug("\(message, privacy: .public)")
case .info: logger.info("\(message, privacy: .public)")
case .warning: logger.warning("\(message, privacy: .public)")
case .error: logger.error("\(message, privacy: .public)")
}
}
}

Pass it in your options in debug builds only:

#if DEBUG
let gronaLogger: (any GronaLogger)? = AcmeGronaLogger()
#else
let gronaLogger: (any GronaLogger)? = nil
#endif
let options = try GronaOptions(
workspaceKey: "acme-market",
configKeys: ["acme-market-2026-10": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."],
logger: gronaLogger
)

GronaLogLevel has four levels: .debug, .info, .warning and .error. Messages start with Grona:. The SDK never passes customer ids, basket contents, tokens or keys to the logger, so marking the message public is safe. The protocol requires Sendable: the SDK calls your logger off the main actor, so keep it thread-safe and away from your views.

Warning What to fix
the <placement> anchor is hidden or ... is fully transparent A UIKit slot or decision anchor is hidden or has an alpha of 0. Keep it visible at zero height when empty, or the control arm is never measured. Logged once per anchor, in debug builds only. The SDK cannot detect .hidden() or .opacity(0) in SwiftUI, so check those yourself.
identify ignored: customer id must match ... The customer id has characters outside [A-Za-z0-9._:-] or is longer than 128.
identify: an all-digit id of 9 or more characters ... The id looks like a phone number or national id. Pass an opaque internal id.
profile ignored: it is not bound to the identified customer setProfile came before identify, or for another customer.
preview link rejected: ... The preview link is malformed. See Preview links.
start ignored: Grona does not run in app extensions Grona.start ran in an app extension.
presentDebugView ignored: GronaDebug was compiled without DEBUG The debug view was called from a build that compiles packages without DEBUG.
  • Empty slots. In Xcode’s view debugger, an empty slot is visible with zero height. No hidden-anchor warning appears.
  • Campaign state. Each campaign you expect is live in the debug view, or shows a reason you understand.
  • Basket. Progress bars and substitutes follow the cart as you change it. If not, a cart path is missing setBasket.
  • Consent. Under .unknown no events are sent. After .granted, events reach your events endpoint.
  • Links. A preview link opens the pinned variant both when the app is running and when the link launches it.
  • Release build. Archive a release build: it compiles with your #if DEBUG code removed, and Generate Privacy Report in the Organizer lists Grona’s privacy manifest.

To pick up a configuration change during a test without waiting, call Grona.refresh with a completion. See Refresh on demand. To see one variant, use a preview link or a local pin.