Skip to content

[feat] Add new user-level custom events API to SDK #1552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: 5.4.0_beta_branch
Choose a base branch
from

Conversation

nan-li
Copy link
Contributor

@nan-li nan-li commented Apr 7, 2025

Description

One Line Summary

Add new feature of user-level custom events API to the SDK, initially releasing as an alpha on the next minor version of 5.4.0.

Details

⚠️ This PR is not ready for an alpha release as the endpoint is still undergoing WIP authentication changes. Omitting authentication will currently encounter a 403 response. ⚠️

Motivation

Add new feature requested that will improve workflows of clients using our SDK.

API

// Method signature
func trackEvent(name: String, properties: [String: Any]?)

// Swift Usage
OneSignal.User.trackEvent(name: String, properties: [String: Any]?)

// Objective C Usage
[OneSignal.User trackEventWithName:@"event" properties:@{@"foo1": @"bar"}];

// Invalid Usage - no op, logs error
let properties = [
    "swiftKey1": "value1",
    "foobar": Foobar() // not valid JSON Object
    "foobar": Date() // not valid JSON Object
]
OneSignal.User.trackEvent(name: "event", properties: properties)

// We will always add an os_sdk dict
let finalPayload = [
    // ... other properties defined by the developer
    "os_sdk" : {
        "sdk" : "050210",
        "type" : "iOSPush",
        "device_os" : "18.1",
        "device_type" : "ios",
        "app_version" : "1.4.4",
        "device_model" : "Simulator iPhone"
    }
]

Scope

  • New API introduced on the User namespace
  • Custom events use the same Delta - OpRepo - Executor infrastructure as other user updates
  • They are cached and retried for retry-able error responses
  • Limitation: Retrying when payload is too large is not yet implemented
✅ Click to see example payload
POST to https://api.onesignal.com/apps/<APP_ID>/integrations/custom_events

{
    "events": [
        {
            "onesignal_id": "6042e183-xxx",
            "payload": {
                "dict": {
                    "string": "somestring",
                    "number": 5,
                    "bool": false,
                    "dateStr": "07/04/2025"
                },
                "os_sdk": {
                    "type": "iOSPush",
                    "device_model": "Simulator iPhone",
                    "device_type": "ios",
                    "app_version": "1.4.4",
                    "sdk": "050210",
                    "device_os": "18.1"
                },
                "anotherDict": {
                    "foo": "bar",
                    "booleanVal": true,
                    "float": 3.1400001049041748
                }
            },
            "timestamp": "2025-04-07T17:38:14Z",
            "name": "event with nested dictionary"
        },
        {
            "timestamp": "2025-04-07T17:38:14Z",
            "name": "event with empty properties",
            "payload": {
                "os_sdk": {
                    "sdk": "050210",
                    "device_type": "ios",
                    "app_version": "1.4.4",
                    "type": "iOSPush",
                    "device_model": "Simulator iPhone",
                    "device_os": "18.1"
                }
            },
            "onesignal_id": "6042e183-xxx"
        },
        {
            "onesignal_id": "6042e183-xxx",
            "name": "event with null properties",
            "timestamp": "2025-04-07T17:38:14Z",
            "payload": {
                "os_sdk": {
                    "sdk": "050210",
                    "device_type": "ios",
                    "device_model": "Simulator iPhone",
                    "device_os": "18.1",
                    "app_version": "1.4.4",
                    "type": "iOSPush"
                }
            }
        },
        {
            "timestamp": "2025-04-07T17:38:14Z",
            "name": "some event",
            "onesignal_id": "6042e183-xxx",
            "payload": {
                "int": 99,
                "dict": {
                    "abc": "def"
                },
                "os_sdk": {
                    "type": "iOSPush",
                    "app_version": "1.4.4",
                    "device_model": "Simulator iPhone",
                    "sdk": "050210",
                    "device_os": "18.1",
                    "device_type": "ios"
                }
            }
        },
        {
            "timestamp": "2025-04-07T17:38:14Z",
            "payload": {
                "foo1": "bar",
                "os_sdk": {
                    "sdk": "050210",
                    "type": "iOSPush",
                    "device_os": "18.1",
                    "device_type": "ios",
                    "app_version": "1.4.4",
                    "device_model": "Simulator iPhone"
                }
            },
            "onesignal_id": "6042e183-xxx",
            "name": "another event"
        }
    ]
}

Testing

Unit testing

None added currently

Manual testing

See commit d4b6c8 for manual test examples.

iPhone 16 simulator on iOS 18.1
Manual testing was done on the simulator with the following scenarios:

Typical Usage

  1. Add a few events at once
  2. They get combined into one payload and sent.

No network connection

  1. SDK already has a user but there is no network connection
  2. Add events but they cannot be sent
  3. Kill app and re-open with connection turned on
  4. Events are read from cache and sent, and removed from cache

New app install without network connection

  1. New install without internet and add events
  2. The event Delta objects are blocked due to no OSID.
  3. Kill app and reopen with connection on
  4. Deltas are uncached and sent after user is created to the backend

New app install with multiple users

  1. New install without internet and add events to the current anonymous user
  2. Login to nan and add events
  3. Event Deltas are blocked due to no OSID
  4. Kill app, reopen with internet turned on
  5. Deltas are uncached and after user creation, events for first user is sent to correct first user, and events for nan are sent to the correct user.

Using the API with Valid + Invalid Properties

  1. Test with nil properties is fine
  2. Test with empty dict for properties is fine
  3. Basic types (string, number, bool) and nested dictionaries are fine
  4. Non JSON object types like Date or a class fails and no-op, logs error

How to Test this Feature

  1. The endpoint still requires auth so in the meantime, hardcode your app's API Key to the header in OSUserRequest here to bypass the 403 response:
additionalHeaders["Authorization"] = "Key YOUR_KEY_HERE"
  1. Add events using the new APIs in either Objective-C or Swift (try valid and invalid inputs):
// Swift Usage
OneSignal.User.trackEvent(name: String, properties: [String: Any]?)

// Objective C Usage
[OneSignal.User trackEventWithName:@"event" properties:@{@"foo1": @"bar"}];
  1. Check the logs for custom events processing and requests made
  2. You can test caching and retrying as well by turning on and off internet, etc.

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

This change is Reviewable

nan-li added 5 commits April 7, 2025 10:44
Motivation:
- previously it would log like this: "request failed with error: <OneSignalClientError: 0x600000cd49c0>
- now it will log like this: "request failed with error: <OneSignalClientError code: 202, message: Error parsing JSON, response: (null), underlyingError: Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set. around line 1, column 0." UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set. around line 1, column 0., NSJSONSerializationErrorIndex=0} >"
* No need to be on protocol nor exposed
* Add button at the bottom of Dev App called "Track Custom Events"
* Add example custom events in Objective-C and Swift
@nan-li nan-li force-pushed the feat/custom_events branch from 2baf788 to d4b6c8b Compare April 7, 2025 18:01
@nan-li nan-li requested review from jkasten2 and jinliu9508 April 7, 2025 19:19
Copy link
Member

@jkasten2 jkasten2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code seems good for an alpha release, there are some TODOs in the code that should be addressed for a beta release. I left a nit small comments.

import OneSignalOSCore

class OSRequestCustomEvents: OneSignalRequest, OSUserRequest {
var sentToClient = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentToClient isn't clear to me what it is for.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, it's a property on the OSUserRequest protocol and you need to have it in each conforming type. I could have better in-code docs in the OSUserRequest protocol about these properties, but sentToClient means this request has been sent to the client and the client will handle retrying with backoff before returning to the executors, so don't send it again while it is doing that. This will change as I plan to refactor the Operation Repo on iOS.

}
self.identityModel = identityModel
self.stringDescription = "<OSRequestCustomEvents with parameters: \(parameters)>"
super.init()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, calling super.init() seems out of place in the middle of the method, I would recommend calling it at the top or bottom of method.

Copy link
Contributor Author

@nan-li nan-li Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it feel weird but it needs to be there in the middle. The stuff above it are properties on this class and the ones below are on the superclass.

  • If you put it at the top, you'll get an error like Property 'self.stringDescription' not initialized at super.init call
  • If you put it at the bottom, it'll error with 'self' used in property access 'parameters' before 'super.init' call

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants