You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
With Config.sendNewSessionBeganSignal set to true, TelemetryDeck.Session.started fires on every process launch, including background-only cold launches triggered by BGAppRefreshTask, HKObserverQuery background delivery, content-available push notifications, significant-location-change wakes, etc.
The SDK does not distinguish a user-initiated foreground launch from a system-scheduled background launch that the user never sees. As a result, session counts on the TelemetryDeck dashboard are inflated by launches the user never initiated, and any metric derived from sessions (shares-per-session, screens-per-session, average session duration, retention) is skewed.
Platform: iOS (most impactful), also affects tvOS and watchOS by the same mechanism
Reproduction
Integrate TelemetryDeck's SwiftSDK 2.12.0 with default config (Config.sendNewSessionBeganSignal == true) into an iOS app target.
Register a background entry point. Easiest reproductions:
HKObserverQuery + HKHealthStore.enableBackgroundDelivery(...) for any HealthKit type, frequency .hourly
BGAppRefreshTaskRequest registered via BGTaskScheduler
Content-available push notification
Background the app and let iOS terminate the process (force from app switcher or wait for memory pressure).
Wait 5 minutes (SDK threshold for registering a new session)
Trigger the background entry point — wait for the HealthKit observer to fire, or use Xcode → Debug → Simulate Background Fetch, or send a push.
Observe TelemetryDeck.Session.started on the dashboard for that wake-up. The user never opened the app.
Expected behavior
Session signals should correspond to user-initiated sessions — cold foreground launches and returns from background after the configured idle threshold. Background-only process launches should not emit Session.started by default.
This matches what the property name suggests (sendNewSessionBeganSignal) and what consumers reasonably expect from a "session" metric.
Actual behavior
Session.started fires unconditionally 0.5s after TelemetryManager.init (which runs on every process launch via @main App.init()). For an app with hourly HKObserverQuery delivery across 5 HealthKit types plus a 5-minute BGAppRefreshTask, this can produce 20–30+ spurious "sessions" per device per day on top of the user's actual visits.
privatefunc startSessionAndObserveAppForegrounding(){
// initially start a new session upon app start (delayed so that `didSet` triggers)
DispatchQueue.main.asyncAfter(deadline:.now()+0.5){TelemetryDeck.generateNewSession()}...}
The call to TelemetryDeck.generateNewSession() is unconditional. There is no check against UIApplication.shared.applicationState, scene phase, or any other foreground/background discriminator.
This behavior is unsuitable for any app that does meaningful work in the background, which is most non-trivial iOS apps today (HealthKit, location, audio, BGTaskScheduler, push, etc.).
Impact
Session counts inflated in proportion to how aggressively iOS schedules background work for the app.
Funnel / retention / engagement metrics distorted. Any per-session metric is undercounted in proportion to inflated denominators.
Average session duration skewed downward. SessionManager's 1-second timer typically only ticks once or twice before iOS suspends a background launch, dragging the average toward zero.
Particularly painful for health, fitness, location, and notification-driven apps — exactly the categories where TelemetryDeck is most attractive.
Screenshots
TelemetryDeck's Dashboard shows inflated session counts for users of an iOS app that does background wake-ups.
TelemetryDeck
AppStore Connect
Note
While AppStore analytics are opt-in only, it is safe to assume that 25-30% of users choose to enable sharing analytics with 3rd party app developers.
Suggested fix
Ideally, there should be a configuration property to enable this behavior, with background sessions being discounted by default:
publicfinalclassTelemetryManagerConfiguration:@uncheckedSendable{...
/// If `true` the TelemetryDeck SDK will count system-scheduled background launches as user sesssions.
publicvarregisterBackgroundSessions:Bool=false...}
The minimal change, in startSessionAndObserveAppForegrounding():
privatefunc startSessionAndObserveAppForegrounding(){
// initially start a new session upon app start (delayed so that `didSet` triggers)
DispatchQueue.main.asyncAfter(deadline:.now()+0.5){#if os(iOS) || os(tvOS) || os(visionOS)guardTelemetryManager.shared.configuration.registerBackgroundSessions
|| TelemetryEnvironment.isAppExtension
|| UIApplication.shared.applicationState !=.background else{return}#endifTelemetryDeck.generateNewSession()}
// ... existing notification observers unchanged ...
}
Notes on this fix:
At App.init(), UIApplication.shared.applicationState is .inactive for foreground cold launches and .background for background-only ones. The 0.5s delay doesn't flip this — a background launch has no scene to activate, so it remains .background for the lifetime of the wake-up.
The existing willEnterForeground observer (TelemetryClient.swift:482-488) is unchanged. If the user later opens the app from a background-launched process, a session fires there after the 5-minute idle check — no behavior lost.
registerBackgroundSessions = false is the new default and is a behavior change; consumers who prefer the old count-every-launch semantics can opt back in with a single flag. App extensions keep current behavior regardless (short-circuited via TelemetryEnvironment.isAppExtension, since UIApplication.shared is unavailable there).
Temporary Workaround
Until the fix lands, consumers can:
Set config.sendNewSessionBeganSignal = false and config.sessionStatsEnabled = false in their TelemetryDeck.Config.
Track "last backgrounded date" in their own persisted state (so the idle check survives process death).
From their SwiftUI scene-phase .active handler, check the idle threshold; if exceeded, briefly flip the two flags back to true, call TelemetryDeck.generateNewSession(), then flip them back to false. The SDK's sessionID.didSet reads the flags synchronously in the same call, so the SDK emits its native TelemetryDeck.Session.started.
This works because TelemetryDeck.Config is a public final class and TelemetryManager retains the reference rather than copying property values, so post-initialize mutations are visible to the SDK on subsequent reads.
The workaround correctly produces zero Session.started signals from background-only launches because SwiftUI scene phase never reaches .active during background tasks.
Summary
With
Config.sendNewSessionBeganSignalset totrue,TelemetryDeck.Session.startedfires on every process launch, including background-only cold launches triggered byBGAppRefreshTask,HKObserverQuerybackground delivery, content-available push notifications, significant-location-change wakes, etc.The SDK does not distinguish a user-initiated foreground launch from a system-scheduled background launch that the user never sees. As a result, session counts on the TelemetryDeck dashboard are inflated by launches the user never initiated, and any metric derived from sessions (shares-per-session, screens-per-session, average session duration, retention) is skewed.
SDK Version
Sources/TelemetryDeck/TelemetryClient.swift,Sources/TelemetryDeck/Helpers/SessionManager.swiftReproduction
Config.sendNewSessionBeganSignal == true) into an iOS app target.HKObserverQuery+HKHealthStore.enableBackgroundDelivery(...)for any HealthKit type, frequency.hourlyBGAppRefreshTaskRequestregistered viaBGTaskSchedulerTelemetryDeck.Session.startedon the dashboard for that wake-up. The user never opened the app.Expected behavior
Session signals should correspond to user-initiated sessions — cold foreground launches and returns from background after the configured idle threshold. Background-only process launches should not emit
Session.startedby default.This matches what the property name suggests (
sendNewSessionBeganSignal) and what consumers reasonably expect from a "session" metric.Actual behavior
Session.startedfires unconditionally 0.5s afterTelemetryManager.init(which runs on every process launch via@main App.init()). For an app with hourlyHKObserverQuerydelivery across 5 HealthKit types plus a 5-minuteBGAppRefreshTask, this can produce 20–30+ spurious "sessions" per device per day on top of the user's actual visits.Root cause
Sources/TelemetryDeck/TelemetryClient.swift:438-442:The call to
TelemetryDeck.generateNewSession()is unconditional. There is no check againstUIApplication.shared.applicationState, scene phase, or any other foreground/background discriminator.This behavior is unsuitable for any app that does meaningful work in the background, which is most non-trivial iOS apps today (HealthKit, location, audio,
BGTaskScheduler, push, etc.).Impact
Screenshots
TelemetryDeck's Dashboard shows inflated session counts for users of an iOS app that does background wake-ups.
Note
While AppStore analytics are opt-in only, it is safe to assume that 25-30% of users choose to enable sharing analytics with 3rd party app developers.
Suggested fix
Ideally, there should be a configuration property to enable this behavior, with background sessions being discounted by default:
The minimal change, in
startSessionAndObserveAppForegrounding():Notes on this fix:
App.init(),UIApplication.shared.applicationStateis.inactivefor foreground cold launches and.backgroundfor background-only ones. The 0.5s delay doesn't flip this — a background launch has no scene to activate, so it remains.backgroundfor the lifetime of the wake-up.willEnterForegroundobserver (TelemetryClient.swift:482-488) is unchanged. If the user later opens the app from a background-launched process, a session fires there after the 5-minute idle check — no behavior lost.registerBackgroundSessions = falseis the new default and is a behavior change; consumers who prefer the old count-every-launch semantics can opt back in with a single flag. App extensions keep current behavior regardless (short-circuited viaTelemetryEnvironment.isAppExtension, sinceUIApplication.sharedis unavailable there).Temporary Workaround
Until the fix lands, consumers can:
config.sendNewSessionBeganSignal = falseandconfig.sessionStatsEnabled = falsein theirTelemetryDeck.Config..activehandler, check the idle threshold; if exceeded, briefly flip the two flags back to true, callTelemetryDeck.generateNewSession(), then flip them back to false. The SDK'ssessionID.didSetreads the flags synchronously in the same call, so the SDK emits its nativeTelemetryDeck.Session.started.This works because
TelemetryDeck.Configis a public final class and TelemetryManager retains the reference rather than copying property values, so post-initialize mutations are visible to the SDK on subsequent reads.The workaround correctly produces zero
Session.startedsignals from background-only launches because SwiftUI scene phase never reaches.activeduring background tasks.