chucker icon indicating copy to clipboard operation
chucker copied to clipboard

Redesign HTTP transaction model

Open MiSikora opened this issue 4 years ago • 19 comments

:warning: Is your feature request related to a problem? Please describe

Right now model for an HTTP transaction is a big structure that kind of serves as bag for everything. It makes it hard to work on new features that require new fields, different encoding format etc. Some links - #204, #202, #69.

:bulb: Describe the solution you'd like

I believe that there should be a separate table for transactions and different types of requests and responses. This way it would be easier to add new types of compression and so on.

This would require to not rely in UI layer on DB entities. They should be rather mapped to some common model (most likely a sealed class or an interface) that would be renderable on the view layer.

It would also allow to tidy a bit nullability of some fields. While request or response still might be null, some of their fields shouldn't be (like i.e. URL address).

:bar_chart: Describe alternatives you've considered

An interesting alternative would be to split HTTP transaction just into three tables (transaction, request, response) and store data as a blob which would be defined in a proto file. There should be no issue with backward compatibility as this is one of main advantages of protobuf. Additional benefit is that new tables would not appear for every new feature and only the proto schema would change.

:raising_hand: Do you want to develop this feature yourself?

  • [x] Yes
  • [ ] No

I could work on it but it is rather a big change proposal that requires some discussion, design and approval beforehand. Not sure what the best way would be. This ticket, draft PR, Slack channel or maybe something else?

MiSikora avatar Mar 01 '20 10:03 MiSikora

If I read you correctly, you're suggesting we treat the HTTP entities as immutable. If so, I'm totally in for this.

Currently we do have a transaction table that stores a HTTP entity both for request and response and we update it once we collect the data. Ideally having separate tables will allow to make sure we treat those entities as immutable (with all the benefits of immutability).

This would require to not rely in UI layer on DB entities. They should be rather mapped to some common model (most likely a sealed class or an interface) that would be renderable on the view layer.

Can you elaborate further? Or provide an example (e.g. adding SSL/TLS information as done in #252). How that would look like in your suggestion.

Moreover, let's put Testing in the roadmap as we have much to improve on that side 👍

cortinico avatar Mar 03 '20 22:03 cortinico

Moreover, let's put Testing in the roadmap as we have much to improve on that side 👍

I thought that we already agreed about it in Slack. On 21st of January I wrote such message for plans after 3.1.0:

  • Cover library with tests completely and force tests run on each PR in our pipeline. We could start with api package as it is the core functionality and move up to UI as a final stage. We definitely need tests to be sure in quality as well as get more credibility from our users.
  • Add some tool to analyze coverage. Initially I thought about Codecov (https://codecov.io/), but now I think that Codacy (https://www.codacy.com/) might work better for us, since we could incorporate more quality checks and reports. Still would like to hear more feedback on this point.

vbuberen avatar Mar 04 '20 08:03 vbuberen

If I read you correctly, you're suggesting we treat the HTTP entities as immutable. If so, I'm totally in for this.

Yes, this would be one thing to do but not the main one. I think that model should be separated into at least 3 tables. Transaction - with reference to a request and a response. Request with all the data for it. And Response with all the data for it.

One step further would be to divide it to stuff like GrpahQlResponse, ProtoResponse, ImageResponse, Multi-Part Response, PlainTextResponse and so on. Here is where I'm not sure what the best approach would be. Maybe it adds too much complexity. On the other hand it is easier to add new requests and responses as the model that represents it does not have to bloat with multiple nullable fields to conform to every transaction type.

Another option that I was thinking of is to not create multiple tables but one with just a blob as a field that would be proto encoded data.

This would require to not rely in UI layer on DB entities. They should be rather mapped to some common model (most likely a sealed class or an interface) that would be renderable on the view layer.

Can you elaborate further? Or provide an example (e.g. adding SSL/TLS information as done in #252). How that would look like in your suggestion.

Yes. Basically repositories would not return database entities but some concrete domain type that can be displayed in the UI. So there would be type (class or interface) like ChuckerTranscation that would have methods and fields that can be displayed on the UI level. It would require mapping from database classes to ChukcerTransaction type which would be responsibility of the repository.

ChuckerTransaction might be as simple as an ordered map or something more complex. Not sure really right now. Also, I can't envision right now how to make it highly configurable and not Android dependent. Easiest way might be to make it depend on the resources.

  • Add some tool to analyze coverage. Initially I thought about Codecov (https://codecov.io/), but now I think that Codacy (https://www.codacy.com/) might work better for us, since we could incorporate more quality checks and reports. Still would like to hear more feedback on this point.

Do you have any code coverage target in mind?

MiSikora avatar Mar 04 '20 12:03 MiSikora

Do you have any code coverage target in mind?

I think we should start with some realistic and easy to achieve numbers like 60% and gradually bump it by 5-10% depending on availability of free time and amount of changes we might have.

vbuberen avatar Mar 05 '20 09:03 vbuberen

Would like to return to this topic. I am all in for splitting our current model into Request and Response to avoid such god objects, which are hard to compare, etc. I am also not sure on what would be the best approach in case of splitting Response further, so can't give my opinion there at the moment.

vbuberen avatar Mar 20 '20 21:03 vbuberen

To be honest, to make it simple, I would just divide and conquer and for now just split into three separate entities - Transaction, Request, Response with the same data that is currently present (maybe with more thought behind nullability of fields). After this step it will be easier to think of a next step and plan it when necessary.

MiSikora avatar Mar 21 '20 01:03 MiSikora

After this step it will be easier to think of a next step and plan it when necessary.

Agree on having this as an intermediate step. It will make things easier for us to handle and we can make this change incremental.

cortinico avatar Mar 21 '20 18:03 cortinico

@vbuberen Can I assign myself to it? If you don't work on it, I think I could start working on it this or next month. I'd provide here my general plan on approaching it because there are some things I want to get feedback on.

MiSikora avatar Jan 08 '22 19:01 MiSikora

Yes, feel free.

vbuberen avatar Jan 08 '22 20:01 vbuberen

My plan for approaching it is as follows.

Introduce new persistence models. These models would be pure data classes, and I want to remove all logic like URL formatting, things specific to HAR, etc. They would use Room but with an in-memory database for now. I would use it as an initial step that would allow me to test everything gradually. I want to use OkHttp with custom converters for this layer. Some initial concepts are below (with omitted annotations).

internal data class HttpCall(
    val id: Long,
    val request: HttpRequest?,
    val response: HttpResponse?,
)

internal sealed interface HttpBody {
    data class Decoded(val value: String) : HttpBody
    data class Encoded(val value: ByteString) : HttpBody
}

internal data class HttpRequest(
    val id: Long,
    val url: HttpUrl,
    val method: String,
    val redactedHeaders: Headers,
    val unredactedHeadersByteSize: Long,
    val body: HttpBody?,
)

internal data class HttpResponse(
    val id: Long,
    val protocol: String,
    val body: HttpBody?,
)

After this is covered, I'd like to see what is needed to deliver the results to the UI. New persistence models should be enough for our purposes, and I don't anticipate a need for any separate presentation model (at least not for this task, maybe for #204). Firstly I'd map old models to new ones and integrate them with the UI. If that works, I'd swap old data providers with new ones, remove mappings, and hopefully remove legacy code.

I would most likely drop the LiveData in favor of Flow to handle issues with the database access itself - #456.

Doing this gradually would allow us to tackle the main issue, but also split ChuckerInterceptor tests into logical units, and most likely open the possibility to handle a couple of the other tasks like: #570 #579 #744

MiSikora avatar Jan 12 '22 16:01 MiSikora

Sounds good to me.

vbuberen avatar Jan 12 '22 18:01 vbuberen

Looks good to me. The only thing that comes to my mind is:

val redactedHeaders: Headers, val body: HttpBody?,

I suppose we're going to write wrappers for those as well, right? Ideally let's get rid of okhttp3.Headers inside the data model: https://github.com/ChuckerTeam/chucker/blob/1f7d9074af2cc19cc159b04ef1fb7ecc91b334de/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/entity/HttpTransaction.kt#L15 and related as this is making hard to adapt the library to other HTTP libraries.

cortinico avatar Jan 14 '22 18:01 cortinico

Random thoughts not directly connected with models redesign.

What about adding some pagination to our DB, so we would be pretty efficient in transactions list?

vbuberen avatar Jan 14 '22 20:01 vbuberen

Looks good to me. The only thing that comes to my mind is:

val redactedHeaders: Headers, val body: HttpBody?,

I suppose we're going to write wrappers for those as well, right? Ideally let's get rid of okhttp3.Headers inside the data model:

I was actually thinking about reusing OkHttp types (though HttpBody is a custom type here). I can add our own types to at least start decoupling if you think it will add value. Personally, I don't see much benefit in it because I don't think that #224 is anywhere near on anyone's radar.

MiSikora avatar Jan 25 '22 12:01 MiSikora

Personally, I don't see much benefit in it because I don't think that #224 is anywhere near on anyone's radar.

Agree that there is no immediate value at this stage, but as we're redesigning the data model, having more decoupling will help tackle future scenarios. As the mobile space is quickly evolving (KMP and co.), I would do the extra effort to try to have a vanilla data model.

cortinico avatar Jan 25 '22 13:01 cortinico

Hi All, 👋 Really excited about this change. Willing to extend an extra hand to get this feature implemented. Let me know how can I help

ArjanSM avatar Jul 06 '22 01:07 ArjanSM

@vbuberen Can I assign this to me? I'd like to work on this a bit 👍

cortinico avatar Sep 17 '22 19:09 cortinico

Yes, feel free to grab it.

vbuberen avatar Sep 18 '22 15:09 vbuberen

Providing a small update here as I've been working on this for long time now.

This is going to be a massive rework of the internals. I'm not sure I will be able to split into smallers PRs at first. I hope to be able to send a draft PR with the whole rework before the end of the year + if there is an agreement from @vbuberen I'll work on splitting it in some form afterwards.

cortinico avatar Nov 18 '22 14:11 cortinico