Tracking Events
Events are the raw material for everything in Joryio: segments, journey triggers, campaign filters, and analytics all run on the events your apps send. This guide covers the conventions and mechanics that are shared across all SDKs - Web, iOS, Android, and React Native. For installation and platform-specific setup, see the individual SDK pages.
How tracking works
Every SDK follows the same pipeline:
- You call
track(eventName, properties)in your app. - The SDK queues the event locally (with offline persistence) and sends it in batches - by default every 5 seconds or every 50 events, whichever comes first.
- The batch is delivered to
POST /v1/track/batch, authenticated with your app's SDK key (formatjry_sdk_<platform>_<random>, one per app - see Apps Overview). - The server resolves the user (anonymous or identified), stores the events, and fans them out to segments, journey triggers, and analytics.
A batch may contain at most 500 events. Timestamps are set by the SDK at call time; the server accepts epoch milliseconds or ISO-8601 strings, and falls back to server time if the timestamp is missing or invalid (so a bad device clock never causes a rejected event).
Event naming conventions
An event name is a string of up to 255 characters. Beyond that, Joryio does not enforce a format - but consistency matters, because event names are how you find events later in segment builders, journey triggers, and the Event Explorer.
Recommendations:
- Use Title Case with spaces. Joryio's built-in events follow the industry-standard e-commerce event taxonomy (Object + past-tense Action, Title Case):
Product Viewed,Product Added,Checkout Started,Order Completedfrom the store integrations, so Title Case custom events (Trial Started,Signup Completed) keep the catalog uniform. Snake case also works - but pick one convention and stick to it; mixing creates duplicate-looking events. - Name the action, not the UI.
Order Completedsurvives a redesign;Green Button Clickeddoes not. - Use object + past-tense verb.
Subscription Upgraded,Video Played,Search Performed. - Keep variability in properties, not names. One
Product Viewedevent with acategoryproperty beats fiftyViewed <Category>events - segments and triggers match on the event name first, then filter on properties. - Avoid overly generic events.
Clickedwith no properties tells you nothing you can act on.
Event names are case-sensitive: order_placed and Order_Placed are two different events.
Properties and data types
Properties are a JSON object attached to each event. Any JSON value is accepted:
| Type | Example | Notes |
|---|---|---|
| String | "currency": "USD" | Also use for dates, as ISO-8601 strings |
| Number | "total": 149.99 | Integers and floats |
| Boolean | "first_order": true | |
| Array | "item_ids": ["SKU-1", "SKU-2"] | |
| Object | "shipping": { "method": "express" } | Nested objects allowed |
Server-side limits per event:
| Limit | Value |
|---|---|
| Event name length | 255 characters |
| Total properties size (serialized JSON) | 50 KB |
| Top-level property keys | 200 |
| Nesting depth | 5 levels |
| Events per batch request | 500 |
Events exceeding these limits are rejected. Property names follow the same advice as event names: pick a convention (product_id, not sometimes productId) and keep types stable - an order_id that is a string in one event and a number in another makes filtering unreliable.
Properties prefixed with $ (such as $platform, $session_id, $app_id) are added automatically - some by the SDKs ($device_id, device data on Session Start) and some by Joryio's ingestion pipeline when the event is received ($app_id, $session_id, $is_identified). Treat that prefix as reserved and do not use it for your own properties.
User attribute key rules
Attribute keys (the object you pass to setAttributes / setAttribute) have
two constraints beyond the property advice above, and a key that breaks either
one is dropped - the rest of the call is stored normally:
| Not allowed | Why |
|---|---|
A . anywhere in the key | A dot is read as a PATH separator, not a character. "profile.email" would store nested profile: { email }, so a segment on profile.email would match nothing and you would have no way to see why. |
A leading $ | Reserved, exactly as for event properties above. |
| An empty key | Nothing to store. |
Keys are dropped rather than renamed on purpose: renaming profile.email to
profile_email would report success while putting your data somewhere you never
query. The dropped keys are named in the server log, and the SDKs warn about them
in your console during integration.
identify vs track
The two core calls do different jobs:
identify(userId)says who the user is. It binds the current device/session to your stable user ID and merges any anonymous history into that profile. Attributes set withsetAttributesdescribe the user (email, plan, name) and live on the profile.track(eventName, properties)says what happened. Properties describe the event, not the user, and are immutable once recorded.
Rules of thumb:
- Call
identifyas early as you know the user - at login, and on app start if a session is restored. Use the same user ID on every platform so web and mobile activity land on one profile (see Multi-Platform Tracking). - Before
identify, events are tracked under an anonymous ID. When you later identify, the server merges the anonymous history into the identified profile, so pre-signup events (first visit, attribution) are not lost. - Use
alias(userId)at signup to explicitly link the anonymous user to the new account, thenidentify(userId). - Put per-occurrence data in event properties (
total,coupon), and durable facts about the person in attributes (plan,lifetime_value). - Call
reset()on logout so the next user on the device does not inherit the profile.
The same event in every SDK
The track call is intentionally identical in shape across SDKs. Here is the same Order Completed event on all four platforms.
- Web (JS)
- iOS (Swift)
- Android (Kotlin)
- React Native
import JoryioSDK from '@joryio/web-sdk';
const joryio = new JoryioSDK({ sdkKey: 'jry_sdk_web_...' });
joryio.track('Order Completed', {
order_id: 'ORD-2024-001',
total: 149.99,
currency: 'USD',
item_count: 3,
coupon: 'SAVE10',
});
Joryio.shared.track("Order Completed", properties: [
"order_id": "ORD-2024-001",
"total": 149.99,
"currency": "USD",
"item_count": 3,
"coupon": "SAVE10"
])
Joryio.track("Order Completed", mapOf(
"order_id" to "ORD-2024-001",
"total" to 149.99,
"currency" to "USD",
"item_count" to 3,
"coupon" to "SAVE10"
))
import Joryio from '@joryio/react-native-sdk';
Joryio.track('Order Completed', {
order_id: 'ORD-2024-001',
total: 149.99,
currency: 'USD',
item_count: 3,
coupon: 'SAVE10',
});
Because the name and properties are identical, one segment condition or journey trigger matches the event no matter which platform it came from.
For standard e-commerce activity (product views, carts, checkout, orders), prefer the SDKs' built-in e-commerce trackers - they emit the standardized event names Joryio's e-commerce features expect. See the cross-SDK E-Commerce Tracking guide.
What happens server-side
Once a batch is accepted, each event:
- Is stored in the analytics store, attached to the resolved user profile (identified or anonymous).
- Is evaluated against journey triggers. A journey whose entry trigger matches the event name (and property filters) enrolls the user immediately - this is how "abandoned cart" or "welcome" journeys start.
- Feeds segments. Event-based segment conditions ("performed
Order Completedin the last 30 days") update from the event stream, and campaigns targeting those segments pick up the change. - Appears in analytics - the Event Explorer, funnels, and per-app analytics.
- Can update the profile. Some events have server-managed side effects: for example,
Session Startevents refresh the user's device record and set profile attributes such ascountry(derived from the request IP).
Two server behaviors worth knowing:
- Bot flagging. Requests from known bot user agents are flagged (events are tagged, and analytics exclude them); profile-mutating calls such as
identifyandsetAttributesare skipped for bots. Traffic from headless browsers in your own test automation may therefore not create profiles. - Lenient validation. Unknown extra fields in the payload are stripped rather than rejected, so an SDK version mismatch does not drop your events.
Verifying and debugging
Check that an event arrived
- Trigger the event in your app.
- In the Joryio dashboard, open Analytics → Event Explorer. Filter by event name and you should see the event within seconds of the SDK flushing its batch (default flush interval: 5 seconds).
- For a per-app view, open Settings → Apps and click View Analytics on the app - it shows total events, the last event timestamp, and top event names, which quickly confirms whether anything from that SDK key is arriving.
If events do not show up
- Force a flush. Events are batched; call
flush()(available in every SDK) to send immediately instead of waiting for the timer. - Enable debug logging. Every SDK has an
enableDebugoption that logs each queued event and each network request with its response. - Check the SDK key. It must match the app's platform -
jry_sdk_web_...for the Web SDK,jry_sdk_ios_...for iOS,jry_sdk_android_...for Android. A regenerated key invalidates the old one immediately. - Check the app is active under Settings → Apps - events sent with the key of a deactivated app are rejected.
- Watch the network response. A batch response includes
successand, on partial failure,failedIndicestelling you which events in the batch were not stored. Oversized properties (over 50 KB, over 200 keys, or nesting deeper than 5 levels) are the usual cause of rejected events. - Web only: the SDK tracking endpoints allow any origin (
Access-Control-Allow-Origin: *), so CORS errors usually mean a wrongapiEndpointor a blocking browser extension - check the console. Also confirm the site runs over HTTPS so localStorage-based queue persistence works. - Testing from automation? Remember bot flagging above - verify with a real browser or device.