Skip to main content

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.

Where to find the data

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.*.

What is not captured automatically

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.

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()

Captured Attributes​

Every emb-push-notification span event includes:

AttributeValueNotes
emb.typesys.push_notificationIdentifies the event type.
notification.typenotif or silentsilent 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:

AttributeSource in the aps payload
notification.titlealert.title or alert.title-loc-key
notification.subtitlealert.subtitle or alert.subtitle-loc-key
notification.bodyalert.body or alert.body-loc-key
notification.categorycategory
notification.badgebadge (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: false unless 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 userInfo payload and capture them manually with the properties: 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.