Skip to main content

Experiments and Feature Flags

Experiments and feature flags

info

Experiments and feature flags are available in the Embrace Apple SDK 6.22.0 and later. If you are on an earlier version, update the SDK before you integrate.

Most apps run A/B tests and roll features out behind flags, but the results usually live in a different tool than your crash, performance, and session data. Embrace closes that gap: when you tell the SDK which experiments and feature flags a user is exposed to, every session, log, crash, and trace produced from that point on carries that context.

That lets you answer questions like:

  • Did the new checkout variant increase the crash rate?
  • Is the app slower to start for users in the treatment group?
  • Which flag was enabled on the sessions where this error spikes?

How it works

You declare enrollment; Embrace does not assign it. The SDK is not an experimentation platform and does not decide which variant a user gets. You keep using whatever system you already have and pass the resulting assignment to Embrace so your telemetry can be segmented by it.

A few properties are worth knowing before you integrate:

  • Declared state belongs to the process, not to a session. Once declared, a record is attached to everything the SDK reports for as long as the app runs, across any session boundaries.
  • It is not persisted across launches. Cold-starting the app clears the declared state, so you have to declare it again on every start.
  • Experiments and feature flags are separate kinds. A record is identified by its kind together with its id, so an experiment and a feature flag can share the id dark-mode and stay independent of each other.
  • The first call wins. The first call for a given id fixes its variant and start time. Later calls for that same id change nothing, even after the record has been ended.
  • Ended records are kept. Ending a record records its end time rather than removing it, so a variant the user was exposed to earlier in the process stays visible in the data.
info

These APIs do nothing until the SDK has been set up. Calls made before EmbraceIO.setup(options:) are ignored rather than queued, so declare enrollment after setup.

Tracking experiments

Use trackExperiment(id:variant:startedAt:) for a single experiment:

import EmbraceIO

EmbraceIO.shared.trackExperiment(
id: "checkout-redesign",
variant: "treatment"
)

Both variant and startedAt are optional. Omitting startedAt uses the moment of the call, which is what you want when you declare enrollment as soon as you resolve it:

// No variant — you only care that the user is in the experiment
EmbraceIO.shared.trackExperiment(id: "new-onboarding")

// Enrollment started earlier, and you are catching Embrace up
EmbraceIO.shared.trackExperiment(
id: "checkout-redesign",
variant: "treatment",
startedAt: assignment.receivedAt
)

When you resolve several assignments at once — the usual case, right after your experimentation SDK finishes fetching — declare them in a single call with trackExperiments(_:):

import EmbraceIO
import EmbraceSemantics

let experiments = assignments.map { assignment in
TrackedExperiment(
id: assignment.key,
variant: assignment.variant
)
}

EmbraceIO.shared.trackExperiments(experiments)

Tracking feature flags

Feature flags work exactly like experiments and follow the same rules. They are kept as a separate kind so the dashboard can present them as a distinct experience:

import EmbraceIO
import EmbraceSemantics

// A single flag
EmbraceIO.shared.trackFeatureFlag(id: "new-video-player", variant: "enabled")

// Several at once
EmbraceIO.shared.trackFeatureFlags([
TrackedFeatureFlag(id: "new-video-player", variant: "enabled"),
TrackedFeatureFlag(id: "offline-mode", variant: "disabled"),
TrackedFeatureFlag(id: "beta-search")
])

Ending an experiment or flag

When a user stops being exposed to an experiment or flag during the same app run, mark it as ended. The record stays in your data with an end time attached, so the end is known rather than inferred:

// A single record
EmbraceIO.shared.untrackExperiment(id: "checkout-redesign")
EmbraceIO.shared.untrackFeatureFlag(id: "new-video-player")

// Several at once, with an explicit end time
EmbraceIO.shared.untrackExperiments(ids: ["checkout-redesign", "new-onboarding"])
EmbraceIO.shared.untrackFeatureFlags(ids: ["offline-mode"], endedAt: userLoggedOutAt)

Untracking is scoped to the kind you call it on: untrackExperiment(id: "dark-mode") leaves a feature flag with the same id untouched. Only the first call for a given id takes effect, and ids that were never tracked are ignored.

You do not need to untrack anything at app exit. The declared state lives only for the current process.

A typical integration

The common shape is to declare enrollment as soon as your experimentation SDK hands you assignments, after Embrace has been set up:

import EmbraceIO
import EmbraceSemantics

func applicationDidFinishLaunching() {
try? EmbraceIO.setup(options: .init(appId: "myApp")).start()

experimentationClient.fetchAssignments { assignments in
EmbraceIO.shared.trackExperiments(
assignments.experiments.map {
TrackedExperiment(id: $0.key, variant: $0.variant)
}
)

EmbraceIO.shared.trackFeatureFlags(
assignments.flags.map {
TrackedFeatureFlag(id: $0.key, variant: $0.value)
}
)
}
}

If a user's assignments change mid-run — after a login, or a remote config refresh — declare the new ids as they appear and untrack the ones that no longer apply. Re-declaring an id you already tracked does nothing, so you can safely call trackExperiments(_:) with your full assignment set after every refresh.

Limits and validation

The SDK bounds what it accepts so that a misconfigured experimentation setup cannot grow your payloads without limit. The defaults are:

LimitDefault
Records held per process500
Identifier length128
Variant length128

Records held per process covers active and ended records together, and experiments and feature flags share the budget. Embrace can adjust these limits remotely, within fixed ceilings.

Validation is per entry, so one bad entry never invalidates the rest of a batch:

  • Surrounding whitespace is trimmed from ids and variants.
  • An id that is empty once trimmed is dropped.
  • An entry whose id or variant is over the allowed length is dropped. Values are never truncated, since a truncated id would refer to a different experiment.
  • nil, an empty string, and a whitespace-only string all mean the same thing for variant: no variant.
  • Duplicate ids within a single call resolve to the first one.

Dropped entries are silent — nothing is thrown and nothing is logged — so it's worth checking your ids and variants against these limits while you integrate.

Where the data shows up

Tracked records are attached as an emb.experiments attribute on the session span and on every log the SDK emits, which is what makes them available for filtering and comparison in the Embrace dashboard. Because the value is also mirrored to storage, a crash report recovered on the next launch still carries the records that were declared when the crash happened.

If you export your telemetry to another backend, the same emb.experiments attribute travels with the exported spans and logs.