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.
Open the debug view
Section titled “Open the debug view”-
Add the
GronaDebugproduct to your app target, as on Install the SDK. -
Import it and call it only inside
#if DEBUG, from a hidden menu or a developer setting:DeveloperMenuViewController.swift #if DEBUGimport Gronaimport GronaDebugimport UIKitfinal class DeveloperMenuViewController: UIViewController {@objc func showGronaDebugView() {Grona.presentDebugView(from: self)}}#endifGronaDebugButton.swift #if DEBUGimport GronaDebugimport SwiftUIstruct GronaDebugButton: View {@State private var isPresented = falsevar body: some View {Button("Grona debug view") { isPresented = true }.sheet(isPresented: $isPresented) { GronaDebugView() }}}#endif -
Tap Refresh on the debug screen after you change something in the app.
What keeps it out of release builds
Section titled “What keeps it out of release builds”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 DEBUGguards. With the import and every call inside#if DEBUG, your release build contains no code that opens the view. Xcode’s app templates set theDEBUGcondition in the Debug configuration only. - GronaDebug’s own check. GronaDebug draws its view only when it was itself compiled with the
DEBUGcondition. OtherwisepresentDebugView(from:)does nothing and logs a warning, andGronaDebugViewis empty.
The debug view has no URL route, so nothing outside your own code can open it.
What the debug view shows
Section titled “What the debug view shows”| 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.
Route logs to the console
Section titled “Route logs to the console”GronaLogger receives SDK diagnostics. Without one the SDK is silent. Send them to the unified logging system in debug builds:
import Gronaimport 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 DEBUGlet gronaLogger: (any GronaLogger)? = AcmeGronaLogger()#elselet 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.
Warnings to look for
Section titled “Warnings to look for”| 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. |
Check the integration
Section titled “Check the integration”- 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
.unknownno 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 DEBUGcode 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.
Next steps
Section titled “Next steps”- Theming for how components look.
- App Store privacy and privacy manifest before you release.