Push Notifications
Push Notifications
The Embrace SDK's PushNotificationCaptureService automatically records when your app receives a push notification or when the user taps one, so notification activity is visible alongside the rest of the session timeline.
How Push Notification Tracking Works
The service swizzles the delegate setter on UNUserNotificationCenter and installs a proxy delegate. When a notification flows through the standard UNUserNotificationCenterDelegate callbacks:
userNotificationCenter(_:willPresent:withCompletionHandler:)— notification displayed while the app is in the foreground.userNotificationCenter(_:didReceive:withCompletionHandler:)— user interacted with a delivered notification.
…the proxy emits an OpenTelemetry span event named emb-push-notification on the currently active session span, then forwards the call to your original delegate. Your delegate continues to run unchanged.
Push activity is not a standalone root span. Look at the session span's events timeline (or search span events by name emb-push-notification). It will not appear when filtering for root spans named emb.push.*.
Only notifications routed through UNUserNotificationCenter delegate methods are captured. Silent / background notifications that your app handles only through UIApplicationDelegate.application(_:didReceiveRemoteNotification:fetchCompletionHandler:) are not intercepted. For those, capture the event manually (see Manual Capture below).
Configuration
The service has a single option, captureData, which controls whether the user-facing payload fields (title, subtitle, body, category, badge) are recorded. It defaults to false.
- EmbraceIO
- Embrace
let services = CaptureServiceBuilder()
.add(.pushNotification(options: .init(captureData: false)))
.addDefaults()
.build()
let options = EmbraceIO.Options.withAppId(
"APPID",
captureServices: services
//...other options
)
try EmbraceIO.setup(options: options)
try EmbraceIO.shared.start()
let services = CaptureServiceBuilder()
.add(.pushNotification(options: .init(captureData: false)))
.addDefaults()
.build()
try Embrace
.setup(
options: Embrace.Options(
appId: "APPID",
captureServices: services
//...other options
)
)
.start()
Captured Attributes
Every emb-push-notification span event includes:
| Attribute | Value | Notes |
|---|---|---|
emb.type | sys.push_notification | Identifies the event type. |
notification.type | notif or silent | silent when the payload contains aps.content-available == 1, otherwise notif. |
When captureData: true, the service additionally reads the aps dictionary and adds the following attributes when the corresponding fields are present:
| Attribute | Source in the aps payload |
|---|---|
notification.title | alert.title or alert.title-loc-key |
notification.subtitle | alert.subtitle or alert.subtitle-loc-key |
notification.body | alert.body or alert.body-loc-key |
notification.category | category |
notification.badge | badge (integer) |
Manual Capture
For notifications that don't pass through UNUserNotificationCenter (for example, background pushes handled in application(_:didReceiveRemoteNotification:fetchCompletionHandler:)), record them yourself using the same event type:
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
if let event = try? PushNotificationEvent.push(userInfo: userInfo) {
Embrace.client?.add(event: event)
}
// ...your handling
completionHandler(.newData)
}
The userInfo dictionary must be APNS-shaped — a top-level aps key is required, otherwise the event is dropped. Minimal payload:
{
"aps": {
"alert": {
"title": "Notification Title",
"body": "Notification Body"
}
}
}
The parser additionally recognizes alert.subtitle, aps.category, aps.badge, and aps.content-available (1 marks the event as silent).
You can also build an event from a UNNotification directly inside a UNUserNotificationCenterDelegate callback:
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let notification = response.notification
if let event = try? PushNotificationEvent.push(notification: notification) {
Embrace.client?.add(event: event)
}
// ...your handling
completionHandler()
}
Both helpers accept an optional properties: [String: String] dictionary that is merged into the event attributes — use this to attach campaign IDs, message IDs, or any other business context you need.
let event = try PushNotificationEvent.push(
userInfo: userInfo,
properties: [
"campaign_id": campaignID,
"message_id": messageID
]
)
Embrace.client?.add(event: event)
Manual events emitted this way honor the SDK's default captureData behavior (payload fields are included). If you want to suppress payload content, construct the event directly with captureData: false.
Best Practices
- Leave
captureData: falseunless you need to read titles or bodies in the dashboard. Payload content frequently contains PII. - If you need campaign or message identifiers, add them server-side as top-level keys in the
userInfopayload and capture them manually with theproperties:parameter — there is no built-in payload filter. - Make sure your app sets a
UNUserNotificationCenter.delegate(the standard requirement to receive foreground notifications). The capture service piggybacks on the same delegate; if no delegate is ever assigned, the proxy still installs but only sees notifications that the system routes through it. - For background-only handlers, add a manual
Embrace.client?.add(event:)call as shown above — otherwise those deliveries will not appear in sessions.