braze-inc / braze-flutter-sdk

Public repo for the Braze Flutter SDK
Other
15 stars 29 forks source link

In App Messages image url not valid (v4.1.0) #49

Closed JordyLangen closed 12 months ago

JordyLangen commented 1 year ago

After updating to 4.1.0 we see the following issue popping up in our Sentry monitoring:

Invalid argument(s): No host specified in URI file:///var/mobile/Containers/Data/Application/224B0CEF-F064-449F-B78D-EE0F12A5B4A5/Library/Caches/com.braze.inappmessage/a2bec79c9885bb2172e0577de4f09b00a034b0ac47d627a89673c977672e4d92/f3fe2c8ef3347780b65da4969640ca326a6b4bd9468673f2a135f2d4ac9217c7.jpg
Crashed in non-app:
http_impl.dart in _HttpClient._openUrl
System
io_client.dart in IOClient.send at line 57 within http
In App
file_service.dart in HttpFileService.get at line 35 within flutter_cache_manager
In App
web_helper.dart in WebHelper._download at line 121 within flutter_cache_manager
In App
web_helper.dart in WebHelper._updateFile at line 103 within flutter_cache_manager
In App
Called from:
<asynchronous suspension>
System
web_helper.dart in WebHelper._downloadOrAddToQueue at line 71 within flutter_cache_manager

We also see the following HTTP requests being made beforehand (which could also be content cards): afbeelding

It only happens on iOS, we do not see a similar issue on Android.

JordyLangen commented 1 year ago

It's (probably) this campaign:

https://dashboard-03.braze.com/engagement/campaigns/641c62574f24c9031c485acb/5b640d667dea0d20180ce281?locale=en

hokstuff commented 1 year ago

Hi @JordyLangen,

We've responded directly on the Support channel with follow up questions to determine the situation in your application. The iOS SDK API we use to determine the location of the assets downloaded is this one (with this property). It looks like from the error message that you are also using a Flutter package called flutter_cache_manager which may be conflicting or inconsistent with the cache we have.

Also, FWIW the file:/// is the prefix that comes from Apple's API, and it appears to be functioning as expected in our sample app.

JordyLangen commented 1 year ago

@hokstuff the reference of flutter_cache_manager is there because we use CachedNetworkImage when showing the images from braze.

We have not changed any logic with regards to our in app message integration, and the flutter_cache_manager has not been updated since 25 feb 2022 in our codebase. The only difference between now and the last time is the braze plugin and it's native counterpart updates. Aside from that the issue is reproducable 100% of the time so I highly doubt it's a caching issue.

You are provided with steps to reproduce over email (I assume as you are aware about the support ticket).

JordyLangen commented 1 year ago

After a chat we learned that there was a breaking change as in app message images are cached by the SDK and thus return a path to a file locally on the device instead of the hosted image url.

We've made the appropriate changes and are now getting this:

afbeelding

Any ideas?

lowip commented 1 year ago

Hi Jordy,

This last issue comes from our Flutter SDK being unable to determine the lifetime of the in-app message and discarding the local asset files earlier than expected.

As mentioned during our call, the long term solution we are currently working on will ensure that you can get access to the data models containing remote URLs. This should allow you to revert to your original implementation.

The fix should be out in the coming days and we'll update here once it's available.

JordyLangen commented 1 year ago

@lowip could you perhaps elaborate on how the caching mechanic works? I personally would've expected that the cached images would have a longer time to live than a couple of (milli)seconds.

lowip commented 1 year ago

Hi @JordyLangen,

Pre Braze Swift SDK 6.0.0 behavior

Internally, our SDK uses the Apple provided URLCache to cache the in-app message assets. That class does not provide direct access to the cached assets on the file system, but instead the Data (a byte array) representation of the assets.

When triggering an in-app message, the SDK:

Our SDK relies on the existence of the in-app message in memory to know when to clean up the working directory. Unfortunately, after transforming the in-app message into a JSON object and sending it to the Flutter layer, the SDK no longer has a reference to the in-app message and therefore cleans up the assets. Due to the asynchronous nature of our SDK, it is possible to load and display the in-app message assets before our SDK cleans them up (the few (milli)seconds you mention).

Braze Swift SDK 6.0.0 behavior

We have just released the Braze Swift SDK 6.0.0 which addresses that issue by moving parts of the assets handling to our UI layer:

The BrazeInAppMessagePresenter.present(message:) protocol method now provides the in-app message before any kind of transformation (i.e. with remote asset URLs) and should allow you to revert the changes made to your integration.

On that topic, we are not sure how you currently handle the in-app messages on the native side. Our sample app provides an example using a delegate method from our UI layer (see here) which does not prevent the Braze provided UI from displaying the message.

If you are simply interested in the in-app message data models, our recommended approach is for you to create your own class conforming to BrazeInAppMessagePresenter and assign it to the braze.inAppMessagePresenter property.

// Define your presenter
class FlutterInAppMessagePresenter: BrazeInAppMessagePresenter {

  func present(message: Braze.InAppMessage) {
    // Pass in-app message data to the Dart layer.
    BrazePlugin.processInAppMessage(message)
  }

}

// When initializing the SDK
braze.inAppMessagePresenter = FlutterInAppMessagePresenter()

Next steps

We are planning a release of the Braze Flutter SDK with the Braze Swift SDK 6.0.0 tomorrow. We will update this thread when it is available.

In the meantime, don't hesitate if you have any questions.

hokstuff commented 1 year ago

Hi @JordyLangen,

We have released Flutter SDK version 5.0.0 which updates the underlying bindings on the iOS side with the implementation updates mentioned above. To get access to the updated remote URLs, you can follow the code snippet here, or you can reference the updated sample app code which implements the method via subclass while using the default Braze UI.

Let us know if this resolves your issues, and feel free to update us if you have any more questions or concerns!

Thank you!

Statyk7 commented 1 year ago

In the snippet here you extend BrazeInAppMessagePresenter but in the sample app code you reference, this is BrazeInAppMessageUI... Am I missing something??

Once you call BrazePlugin.processInAppMessage(message), the In-App Message will be pass through the inAppMessageStreamController on the Flutter side, right?

lowip commented 1 year ago

Hi @Statyk7,

This is accurate, the BrazeInAppMessageUI class is the Braze provided implementation UI. It conforms to the BrazeInAppMessagePresenter protocol.

Our sample app subclasses the BrazeInAppMessageUI to both send the message to the Flutter side and display it with our UI.

Once you call BrazePlugin.processInAppMessage(message), the In-App Message will be pass through the inAppMessageStreamController on the Flutter side, right?

That's correct.

JordyLangen commented 1 year ago

@hokstuff @lowip after upgrading to 5.0.0 we still see that the local asset urls do not yield any actual images. Is that expected?

lowip commented 1 year ago

Hi @JordyLangen,

Could you please provide your implementation used to pass the IAM to the Flutter side (where you are calling BrazePlugin.processInAppMessage(message))?

In the Flutter SDK 5.0.0 (Swift SDK 6.0.0), the local assets URLs follows our In-App Message UI lifecycle. If you do not display the IAM with our UI, the assets working directory will be deleted.

If you would like more control over the local assets URLs destination directory (e.g. to put them in a directory for which the lifetime is controlled by your application), you can make use of the new APIs added in the Swift SDK 6.0.0.

// Define your presenter
class FlutterInAppMessagePresenter: BrazeInAppMessagePresenter {

  func present(message: Braze.InAppMessage) {
    // Working with remote assets URLs
    // - In this method, the message contains remote assets URLs
    // - If you do not need local assets URLs, simply pass the message to the
    //   Dart layer
    BrazePlugin.processInAppMessage(message)

    // ---- OR

    // Working with local assets URLs
    // - You can use the `message.context.withLocalAssets` method to transform
    //   the remote assets URLs stored on the message into a local assets URLs
    //   stored in a directory that you control
    // - You must provide a valid directory URL (ending with a `/`) for the
    //   destinationURL parameter
    // - The Braze SDK will not delete this directory automatically, you are
    //   responsible for its lifecycle.
    message.context.withLocalAssets(message: message, destinationURL: /* ... */) { result in 
      switch result {
      case .success(let message):
        // Pass in-app message data to the Dart layer.
        BrazePlugin.processInAppMessage(message)
      case .failure(let error):
        print("An error occured when retrieving a message local assets: \(error)")
      }
    }

  }

}

// When initializing the SDK
braze.inAppMessagePresenter = FlutterInAppMessagePresenter()

If you want to create a cache directory managed by the OS, you can use the following code:

static func assetsDirectory() throws -> URL {
  try FileManager.default.url(
    for: .cachesDirectory,
    in: .userDomainMask,
    appropriateFor: nil,
    create: false
  )
  .appendingPathComponent("braze-messages-assets", isDirectory: true)
}

This directory is guaranteed to exist for the lifetime of your application, but may be deleted by the OS while your application is terminated if running low on storage space.

Please let us know if you have any other questions.

JordyLangen commented 1 year ago

@lowip We are now using the BrazeInAppMessagePresenter (instead of the BrazeInAppMessageUIDelegate) and that works fine.

However, we also tried with the BrazeInAppMessageUIDelegate (as it meant no changes on our side) but in that case we still observed this issue. We were a bit lost on whether or not we could use the BrazeInAppMessagePresenter as the BrazeInAppMessageUIDelegate allows us to discard the message (which turned out not to be an issue).

As a sidenote, the example in this repository uses the BrazeInAppMessageUI: https://github.com/braze-inc/braze-flutter-sdk/blob/master/example/ios/Runner/AppDelegate.swift apparently another way of showing/handling the in-app-message which added to our confusion if we could safely use that instead of the BrazeInAppMessageUIDelegate (which is also still used in the example)

hokstuff commented 1 year ago

Hi @JordyLangen,

Glad to hear that you are now able to get the remote URLs in your integration.

For context, implementing via BrazeInAppMessagePresenter or via BrazeInAppMessageUI subclass (BrazeInAppMessageUI conforms to the BrazeInAppMessagePresenter protocol) and following the steps described in the comment above will expose the Braze.InAppMessage data model prior to any remote URL transformations.

When using the BrazeInAppMessageUIDelegate method as in our sample app's previous code but while using Flutter SDK 5.0.0, the inAppMessage(_:willPresent:view:) will contain the Braze.InAppMessage data model after remote URL transformation, which is what you were seeing. We will add a specific note in the Changelog entry for 5.0.0 calling this out as well as remove any remnants of BrazeInAppMessageUIDelegate in the sample app AppDelegate since it is no longer used.

Hope this clears things up!

hokstuff commented 12 months ago

I'm closing out this thread since it appears that this specific issue around loading remote URLs for in-app messages is resolved. Please reopen this thread if this isn't the case or follow up in Support or if new issues arise.

Thank you!