Recommended hybrid app tracking strategy

Is there a recommended approach to tracking events in a hybrid app scenario? In our case, some screens are implemented natively (and tracking events using the iOS and Android trackers via the React Native wrapper), while some screens are relying on a WebView to show web pages that we own (and tracking events using the JS tag-based tracker). Especially since WKWebView is getting more strict with client-side cookies, we are finding that our domain user/session ids are getting reset sooner than expected, so we’re wondering if it’s better to get away from relying on cookies to identify users.

The main thing we’re trying to figure out is whether it would be better to:

  1. Continue using the native and web trackers simultaneously (and link them via session ids via tracker configuration and contexts)

Or

  1. Stop tracking events using the JS tag-based tracker when the website is running in the context of a WebView and instead forward all events to the native layer for them to be tracked directly by the native trackers.

Thanks in advance for any help/tips you can provide.

1 Like

Hi @w123,

This is an interesting question. At the moment there isn’t anything in place to allow the interaction between the JS tracker in the WebView and the native tracker in the app.

In my opinion, if the type of events you want to track in the WebView is quite limited you can adopt the second option (forward all events to the native layer). You should build a bridge between the WebView and the native side passing the data there and tracking them with the React Native tracker. The mobile trackers have the PageView event out-of-the-box. They don’t track it automatically but you can fill the event fields with the data from the WebView and track it through the mobile tracker. For other events, not available in the mobile trackers, you should track them using SelfDescribing events passing the schema of the event used in the JS tracker and the data correctly formatted.

On the other hand, if the message passing between WebView and native side is excessively complex, the best solution is to track the data using both trackers simultaneously. The best solution there would be creating a custom entity with a common ID to track all the events of both trackers. That ID will help you to join the data from both trackers in the data modelling phase. Tracking the custom entity on both trackers, using the Global Contexts feature it’s easy to add a static context to all the events tracked in the React Native tracker and the JS tracker. Otherwise, if not already used, you can use the userId of Subject in both trackers passing your own ID. In that case you wouldn’t need to create your own custom entity to track the ID.

3 Likes

Hi @Alex_Benini,

Thank you for the answer! That is very helpful.

Based on what you’ve said, one thing I’m wondering is where to draw the line between a use case that is “limited” vs. “complex”. Would you define that distinction by looking at number of events, size of event payloads, or something else?

I’m also wondering about the performance tradeoffs between the 2 approaches. In my mind one of the benefits of not tracking in the web layer is we can avoid loading the tracker code there. But on the flip side, I’d imagine it will be a performance hit every time we’d need to communicate over the bridge from web → native because we’d have to serialize/deserialize the payloads each time. Do you agree? Would JSON.stringify/JSON.parse be our best approach for the serialization/deserialization of event payloads between the 2 layers? Or would a different approach be more performant? For example, looking at some Snowplow tracker HTTP requests, I noticed that the payloads contain JWTs as part of the payload which contain the event data. Is this just for the sake of making sure payloads have not been tampered with? Or is there a performance reason to do this as well? Edit: thinking some more, since this token is generated client-side I doubt its purpose is to prevent tampering… But I’m still curious what the benefit of it is. Edit: sorry for the confusion… I incorrectly assumed the payload contained a JWT, but upon further review it’s just a base64 encoded string.

In addition, another benefit of not using the JS tracker in my mind is we can get away from cookies and avoid the inflated session issues that come along with client-side cookie restrictions, and as a result have more reliable app session data. However, the possible downside is that we might have performance issues as a result of the serialization/deserialization concern above. Do you agree?

The other thing I wanted to mention about our particular use case is that currently most events are already firing at the web layer against the JS tracker, but our plan is to eventually replace many of our web pages with fully-native screens, in which case all those associated events would have to be sent via the native tracker anyway. So I’m wondering if it would be best to bite the bullet now and just shift all WebView events over to the native tracker via the bridge approach. Or by doing so do you think we will run into performance and/or modeling issues?

Sorry for the late response…

I agree with this point. At the moment, we don’t provide any bridge out-of-the-box so you should implement that by yourself. Serialization and deserialization is computational expensive but much depends on the amount of data to transfer. For example, if you are just interested in the url of the page, how deep the user scrolled down and link clicks, the data transferred in the bridge can be very simple and you can build the full payload for the custom events in the (react) native side of the app. Unfortunately, it requires some work to build the bridge between the two. The advantage is that it can be really tailor-made to your requirements and transfer only the data you really need, reducing the computational cost but obviously increasing the implementation cost.

I agree. There is a trade-off between the complexity in the app with the bridge between webview and native app and the complexity in the data modelling generated by having two different trackers (JS + RN) tracking data of the same user/app. You would end up tracking sessions in the JS tracker and session in the React Native tracker and in the data modelling those data have to be stitched together. I think the latter provides advantages if your app relies a lot on web pages, so much of the user behaviour data comes from the web pages in your hybrid app.

The bridge would seem the optimal solution in this case.

This problem with hybrid apps came out in the past but at the moment we haven’t provided any solution to help with these cases. However, I’ll try to raise the attention to this problem and see if we can prioritise some work to help tracking on hybrid apps without forcing us to build custom bridges between JS and native trackers.

2 Likes

Thanks so much @Alex_Benini, your insight was very helpful! Based on what you’ve said, we’re leaning towards creating a bridge and forwarding the events directly to the native trackers because as you said the modelling will be easier and it won’t suffer from cookie limitations.

Since our app is a React Native app, it seems we have 2 options:

  1. implement a bridge from the WebView to the React Native tracker, which would then send the events to the underlying native (iOS & Android) trackers.

or

  1. implement a bridge from the WebView directly to the native (iOS & Android) trackers.

Between those 2 options we’re leaning towards option 2. Although it is slightly more work to write the native code for both platforms, we’re thinking it will give better performance because React Native’s JS thread won’t need to be busy de-serializing and handling the events for the sole purpose of forwarding them on to the native (iOS & Android) layers (i.e. remove the middleman). Let me know if you can think of any issues with that approach, but if not we’ll go ahead with implementing it.

P.S. Thanks for raising attention to this problem. We’d definitely be interested in leveraging any future improvements that get added to the SDKs. Even though we’ll probably have a custom solution by then it would be good to migrate to Snowplow’s standard solution so we don’t have to maintain our custom solution going forward.

2 Likes

Thanks a lot for your kind words.

It sounds good to me. Maybe you already know but our React Native Tracker is a wrapper around the mobile native trackers. So, technically, you can use them directly as explained here without any extra configuration work or conflict. That is probably another point in favour of your option 2.

@Alex_Benini Thanks for the extra point of clarification. Agreed that it will help support option 2! We are planning to inject scripts into the webview that would parse the web event payloads and then pass those payloads straight through to the mobile native tracker methods, as you mentioned. :+1:

2 Likes

@Alex_Benini Sorry to circle back on this after such a long delay. But as I finally got around to trying to implement the strategy above, it occurred to me how much data the JS Tracker is collecting for free when firing page view events. Realizing this, I’m questioning the feasibility of forwarding these events to the native layer afterall because (1) afaik there isn’t a good way to intercept the resulting web events and forward them as-is, so I’d have to determine all the page view event properties manually which would be quite cumbersome and error prone, and (2) even if I could manage to get all the event payload data, the events themselves would be pretty hefty to send over the bridge. As a result, I thought an alternative strategy might be to continue using the JS Tracker in tandem with the native Mobile Trackers. My idea was to inject the native session context into the webpage, and then associate it with the JS Tracker as a global context during tracker initialization. This way, all webview events could be tied back to their corresponding native mobile session. But as I explored this option, I found that the native Mobile Trackers don’t seem to expose their session context. So my questions now are:

  1. Do the native mobile trackers expose a way to gain access to the session context that I’m just overlooking? If so, this would enable me to inject the mobile session into the webviews and use it as a global context on the js/web tracker.
  2. Does the js/web tracker expose a way to intercept the event payloads and handle the transport myself? If so, this would allow me to gain access to the events and send them to the native mobile trackers rather than going straight to the collector endpoint.
  3. Is there some other approach that I should consider that I’m overlooking?

Thanks in advance for any help.

@Alex_Benini in rereading your earlier post, I just remembered what you said about “a custom entity with a common ID”. If we are not able to get the session context from the native mobile trackers and pass it down to the JS/web tracker, I think the common ID option might be our best bet. My only question is around the modeling phase… Since the JS/web events won’t have the native sessionId associated with them because the session context is not exposed by the native mobile SDKs (afaik), we’d have to rely on timestamps to figure out which native mobile session the JS/web events belonged to, right? Is it guaranteed that the timestamps of the native mobile events and the JS/web events will fall in chronological order? Or is there a chance that the timestamps will be slightly out of order when comparing the mobile and web events? After joining the data using the common ID, will timestamps be our best bet for figuring out which native mobile session the JS/web events belong to? Or is there another way to figure out which native mobile sessions the JS/web events belong to?

Hi @w123 , thanks for your feedback about your current approach and the problem you are solving. This will really help us to implement this feature in the best way.

You can get the session context (client_session entity) from the native mobile trackers as explained in the docs.
Unfortunately, at the moment, this is only available in the native trackers and not in the React Native tracker. With that callback you can get the session_id and pass it to the JS tracker.

Otherwise…

Does the js/web tracker expose a way to intercept the event payloads and handle the transport myself?

The JS tracker offers an interface to implement plugins. In that interface there are two methods beforeTrack and afterTrack which are callbacks where you can get the payload. Unfortunately, this doesn’t block the emission to the collector.

Is it guaranteed that the timestamps of the native mobile events and the JS/web events will fall in chronological order?

The dvce_created_tstamp should be reliable enough as it’s the timestamp generated by the device for both trackers. In the data modelling we usually use the derived_tstamp because it corrects the cases where the device is configured with the wrong time.

Please, let us know any further issue, question or edge case. We have put this feature in our roadmap. I’ll keep you updated here on any news on that.

Hi @Alex_Benini, thanks for the tip about the session context. I was thinking it wasn’t available because there was a line in the v2-to-v3 migration guide that said

Session callbacks have been removed. They will be reintroduced soon in one of the next minor versions. More details will be provided at the release.

That’s great to know the callbacks have been added again in v3.1. Specifically in the case of React Native, I think the iOS SDK is still pinned at "~> 3.0". But if that’s the case I think we’d be fine waiting for the React Native SDK to bump the iOS SDK version before trying to implement this. Edit: I just realized I was reading the semver range wrong… It looks like it does in fact include v3.1. Sorry for the confusion.

And thanks for the tip about dvce_created_tstamp. As you said, if both trackers are sharing the same timestamp, then I think that will suit our needs just fine for determining the order of events.

One last question I have is related to the “common ID” approach that we were discussing above. If we are in fact able to get the the session context (which includes the sessionId and userId (i.e. install id)) from the mobile tracker and can set it as a global context on the JS/web tracker, will we still need a custom common ID and custom entity? I’m assuming we won’t need that now since the session’s sessionId and session’s userId will be sufficient for joining all of our data. Do you agree? Or am I overlooking something? I could see the custom common ID being useful in a case when we needed to initialize the JS/web tracker prior to initializing the mobile tracker, but in our case we are always initializing the mobile tracker prior to loading a WebView. Are there any other gotchas I might be overlooking that would require the need for a custom common ID? For example, are there any race conditions around when the session callback will fire once the mobile SDK is initialized? Or will the callback always fire immediately when the mobile SDK is initialized?

You are right, the common ID is a simple way to stitch together the data generated by both trackers, but in this case the session_id would be already good for that, so as you said you don’t need to generate a new one, you just need to track it in the JS tracker.

The session callback is fired as soon as the first mobile event is generated. You can probably manually track an event before to launch the webview to be sure a session is started.
Another point to consider in this scenario is about the session timeout. If the mobile tracker doesn’t receive events the session could expire. The problem here is to keep alive the mobile native session meanwhile the user interacts with the webview. The session can expire after a long time of inactivity because the tracker doesn’t receive events. In reality, the user is interacting with the webview and the events would be tracked with the JS tracker rather than the mobile tracker. A possible workaround would be tracking sort-of “ping” events in the mobile tracker for the time the user interacts with the webview. This just to keep the mobile session alive.

1 Like

@Alex_Benini Thanks a lot, that is very helpful to know about the session lifecycle and timeout.

Based on what you’ve said w.r.t. the session creation, I’ll be sure to fire a manual event prior to launching a webview.

And based on what you’ve said w.r.t. the session timeout, I’m thinking we can implement a beforeTrack / afterTrack callback on the JS/web tracker that will send a ping event to the native/mobile tracker (via a bridge). This way, the firing of the js/web events can simultaneously serve as a keep-alive mechanism for the native/mobile tracker session. I’m thinking this will be the best way to keep the native/mobile session from expiring while the user is actively using the webview, but it will also allow the native/mobile session to expire due to inactivity as would normally be expected. Edit: I’m also thinking it would be good to throttle the pings being generated in the webview so we don’t flood the bridge and native/mobile tracker with an excessive number of ping events.

How does that approach sound to you?

P.S. As an alternative to generating the ping events in the js/web tracker’s beforeTrack / afterTrack callbacks, I had also contemplated instead just setting up a ping event in the native layer that would run in a looping interval, but I think that would be tricky to get to play nice with the SDK’s built-in session timeout / lifecycle behaviors (i.e. we don’t want to keep the session alive if the user truly isn’t interacting with the app anymore).

1 Like

I fully agree with your strategy. A ping generated by a timer would be easier but for sure less reliable. Using beforeTrack and afterTrack is more complex but it would really behaves like a real session.

1 Like

Ok sounds like a plan! I just have 1 last question. Earlier you mentioned this:

We have put this feature in our roadmap. I’ll keep you updated here on any news on that.

I’m assuming you don’t know any sort of timeline yet, but I figured it can’t hurt to ask. Do you have any visibility into when this feature will get prioritized? I just wanted to make sure it wasn’t going to get prioritized anytime soon, otherwise I might be better off waiting until I can leverage the official Snowplow implementation :smiley:

Sorry for the delay, we recently had a few requests about this feature, I think we should be able to work on that in early Q3 but I’ll give you more details in the next weeks when we have a clear plan for the next quarter.

1 Like

That’s great to hear! Thanks for getting this on the roadmap and thanks again for all the help!

1 Like