Skip to content
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

Add support for client reports #801

Merged
merged 15 commits into from
Oct 21, 2024
Merged

Add support for client reports #801

merged 15 commits into from
Oct 21, 2024

Conversation

savhappy
Copy link
Collaborator

No description provided.

@whatyouhide
Copy link
Collaborator

@savhappy can you lmk when this will be ready for review?

@savhappy
Copy link
Collaborator Author

@whatyouhide yep yep I'll tag you here when ready

@savhappy savhappy force-pushed the sav/client-error-reports branch 2 times, most recently from f7abce9 to eaf55a1 Compare September 30, 2024 16:07
@savhappy savhappy marked this pull request as ready for review October 1, 2024 18:25
@savhappy
Copy link
Collaborator Author

savhappy commented Oct 1, 2024

@whatyouhide ready for an initial review. I've only added a single instance of recording an event but will add in more after discussion

Copy link
Collaborator

@whatyouhide whatyouhide left a comment

Choose a reason for hiding this comment

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

Left a first pass of comments, some are nits, some are things to address, and a couple are blocking. Let me know if you wanna go through them together 🙃

lib/sentry/application.ex Outdated Show resolved Hide resolved
lib/sentry/config.ex Outdated Show resolved Hide resolved
lib/sentry/event.ex Outdated Show resolved Hide resolved
lib/sentry/envelope.ex Outdated Show resolved Hide resolved
lib/sentry/envelope.ex Outdated Show resolved Hide resolved
Comment on lines 61 to 78
def add_discarded_event({reason, category}) do
if Enum.member?(@client_report_reasons, reason) do
GenServer.cast(__MODULE__, {:add_discarded_event, {reason, category}})
end

:ok
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why did you choose to silently ignore events whose reason is not a valid one?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I decided to do this for two reasons -> From what I can tell, the Ruby SDK ignores invalid reasons

    def record_lost_event(reason, data_category, num: 1)
      return unless @send_client_reports
      return unless CLIENT_REPORT_REASONS.include?(reason)

      @discarded_events[[reason, data_category]] += num
    end   

and the documentation says

In case a reason needs to be added, it also has to be added to the allowlist in [snuba](https://github.com/getsentry/snuba/blob/4e7cfdddcf7b93eacb762bc74ca2461cec9464e5/snuba/datasets/outcomes_processor.py#L24-L34).

So we choose to do nothing if it's invalid unless we want to add them to this repo mentioned. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay, sounds good. Let's silently ignore it for now, BUT leave a comment with basically what you just said to me, but in the code 🙃

lib/sentry/client_report.ex Outdated Show resolved Hide resolved
lib/sentry/client_report.ex Outdated Show resolved Hide resolved
lib/sentry/client_report.ex Outdated Show resolved Hide resolved
lib/sentry/client_report.ex Outdated Show resolved Hide resolved
@savhappy
Copy link
Collaborator Author

savhappy commented Oct 6, 2024

Thanks @whatyouhide for this! Updated with these comments except the blocking tests. Will add those shortly. Hold off on more feedback till I get the next round out. I need to add data_category to this somewhere too and maybe a capture_event function to direct how this report is classified.

@savhappy
Copy link
Collaborator Author

savhappy commented Oct 6, 2024

Question @whatyouhide : I need to add in the appropriate data_category to the payload. We add the type only in the headers but other SDK's have a type field. The mappings for types would look like this

defmodule Sentry.DataCategory do
  @spec data_category_mappings(String.t()) :: String.t()
  def data_category_mappings(type) do
    case type do
      "session" -> "session"
      "sessions" -> "session"
      "attachment" -> "attachment"
      "transaction" -> "transaction"
      "profile" -> "profile"
      "span" -> "span"
      "check_in" -> "monitor"
      "statsd" -> "metric_bucket"
      "metric_meta" -> "metric_bucket"
      "event" -> "error"
      "client_report" -> "internal"
      _ -> "default"
    end
  end
end

But we only have the types Event, Attachment, CheckIn, ClientReport (and soon Transaction). So whats the best way to go about deriving the type that will age well as we add more in the future? Or should I just get it based on the struct that's passed and if its not one of the structs see if it fits a string that's passed as the arg for data_category instead? I hope this makes sense. If not we can go into detail on Tuesday.

@whatyouhide
Copy link
Collaborator

@savhappy can you elaborate on the issue here? Where do we need to add data_category? And what is the data category?

@savhappy
Copy link
Collaborator Author

savhappy commented Oct 7, 2024

Yep! @whatyouhide The data categories are used to classify the type of data being ingested (ingest side types for collecting statistics).

It's defined in the Client Report docs as -> category: The data category for which the discard reason applies. These are the same data categories used for [rate limits](https://develop.sentry.dev/sdk/rate-limiting/#definitions).

We need to add the data_category to the payload ->

 "discarded_events": [
    {
      "reason": "queue_overflow",
      "category": "transaction",
      "quantity": 1
    }

So When I send {reason, category} over to the genserver. I need to ensure that I'm sending over the correct data category to Sentry.

In Transport module ->

  def record_discarded_event(reason, category) do
    # add logic for data category here
    # call to client report genserver
    ClientReport.add_discarded_event({reason, category})
    # end

    :ok
  end

Copy link
Collaborator

@whatyouhide whatyouhide left a comment

Choose a reason for hiding this comment

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

Left one more round of comments, this is shaping up! 🎉

lib/sentry/client.ex Outdated Show resolved Hide resolved
lib/sentry/client.ex Outdated Show resolved Hide resolved
@spec start_link([]) :: GenServer.on_start()
def start_link([]) do
# check config to see if send_client_report is true
GenServer.start_link(__MODULE__, %__MODULE__{}, name: __MODULE__)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Using %__MODULE__{} as the state is confusing IMO because you're conflating two responsibilities in the same place: now %__MODULE__{} is both the data definition of the client report, as well as the GenServer's internal state.

Instead, I'll go against my own advice and say: just use the map of discarded events as the GenServer's state, and only add the timestamp when sending the client report. Makes sense?

@send_interval 5000

@spec start_link([]) :: GenServer.on_start()
def start_link([]) do
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you choose to go with Sentry.ClientReport containing both the struct as well as the GenServer, you'll need to make all these functions private (@doc false).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated but double check that this is what you meant ->

@doc false
  @spec start_link([]) :: GenServer.on_start()
  def start_link([]) do
    # check config to see if send_client_report is true
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end


@spec start_link([]) :: GenServer.on_start()
def start_link([]) do
# check config to see if send_client_report is true
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this a TODO?

Copy link
Collaborator Author

@savhappy savhappy Oct 8, 2024

Choose a reason for hiding this comment

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

Yep. If the user chooses to flip to false then we don't report but I'm going to add this here ->

        if Config.dsn() != nil && Config.send_client_reports?() do
          Client.send_client_report(client_report)
        end

in the :send_report handle_info function

@impl true
def handle_cast({:add_discarded_event, {reason, category}}, client_report) do
if map_size(client_report.discarded_events) == 0 do
{:noreply, %{client_report | discarded_events: %{{reason, category} => 1}}}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This code is not needed if you use Map.update/4 the way you do below. If the map is empty, you'll init {reason, category} to the third arg of Map.update/4 (1, correctly). Plz remove the if 🙃


@impl true
def handle_info(:send_report, state) do
if map_size(state.discarded_events) == 0 do
Copy link
Collaborator

Choose a reason for hiding this comment

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

Opposite: if map is empty, then don't report?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Whoops! Yes, thx!

lib/sentry/client_report.ex Outdated Show resolved Hide resolved
end

@spec add_discarded_event({reason(), String.t()}) :: :ok
def add_discarded_event({reason, category}) do
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't use a tuple here, it's not needed. Just pass two args, (reason, category) and call the GenServer cast {:add_discarded_event, reason, category}.

@@ -40,6 +40,11 @@ defmodule Sentry.Transport do
result
end

def record_discarded_event(reason, category) do
ClientReport.add_discarded_event({reason, DataCategory.data_category_mapping(category)})
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why go through the extra data_category_mapping/1 step here rather than just do the mapping in ClientReport, without introducing a new Sentry.DataCategory module?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I believe we resolved this in our pairing. Updates made so lemme know thoughts

@savhappy savhappy force-pushed the sav/client-error-reports branch from e98dc43 to 2503547 Compare October 8, 2024 18:33
@savhappy savhappy requested a review from whatyouhide October 8, 2024 18:35
lib/sentry.ex Outdated
@@ -411,6 +412,7 @@ defmodule Sentry do
|> CheckIn.new()
|> Client.send_check_in(options)
else
# add record dropped event here? Dropped due to....
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ask on Discord because I don't think we need to report these here but maybe other SDKs do. Remove this comment in the meantime so that we can start shipping this PR.

:unsampled

:excluded ->
# do we need to add this if an event is dropped due to it being a duplicate??
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ask in Discord

@@ -34,6 +34,41 @@ defmodule Sentry.Envelope do
}
end

@doc """
Creates a new envelope containing the client report.
since: "10.8.0"
Copy link
Collaborator

Choose a reason for hiding this comment

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

@doc since is an ExDoc feature. I literally meant:

@doc since: "10.8.0"

after the @doc string. There are tons of examples in this repo 🙃

lib/sentry/envelope.ex Show resolved Hide resolved
Comment on lines 50 to 51
if is_binary(type) do
type
Copy link
Collaborator

Choose a reason for hiding this comment

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

When will this ever be a string? We talked about functions having a small "type" surface but this function is still accepting binary | Attachment.t | .... Can we make it so this only accepts a struct OR a binary?

Also, can you add typespecs and tests for this?

Comment on lines 66 to 67
_ ->
"default"
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is dangerous because I can call get_data_category(:completely_banana_value). Make sure the input is either a string or a struct (per comm
ent above)

Comment on lines 43 to 50
_ =
if is_list(event_item) do
Enum.map(event_item, fn event ->
ClientReport.add_discarded_event({reason, Envelope.get_data_category(event)})
end)
else
ClientReport.add_discarded_event({reason, Envelope.get_data_category(event_item)})
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. List.wrap/1
  2. Enum.map/2 wastes memory here, use Enum.each/2 since we ignore the result.
Suggested change
_ =
if is_list(event_item) do
Enum.map(event_item, fn event ->
ClientReport.add_discarded_event({reason, Envelope.get_data_category(event)})
end)
else
ClientReport.add_discarded_event({reason, Envelope.get_data_category(event_item)})
end
_ =
event_item
|> List.wrap()
|> Enum.each(&ClientReport.add_discarded_event({reason, Envelope.get_data_category(&1)}))

@savhappy savhappy requested a review from whatyouhide October 12, 2024 21:03
Copy link
Collaborator

@whatyouhide whatyouhide left a comment

Choose a reason for hiding this comment

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

There are no Sentry.Transport tests for the new functionality

Comment on lines 49 to 50
@spec add_discarded_event(reason(), String.t()) :: :ok
def add_discarded_event(reason, category) do
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is internal for now so @doc false


@spec get_data_category(Attachment.t() | CheckIn.t() | ClientReport.t() | Event.t()) ::
String.t()
def get_data_category(type) do
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you wanna raise, do it like this so that invalid input doesn't even get in the function:

Suggested change
def get_data_category(type) do
def get_data_category(%mod{} = type) when mod in [Attachment, CheckIn, ClientReport, Event] do

@savhappy savhappy requested a review from whatyouhide October 15, 2024 20:08
@savhappy savhappy force-pushed the sav/client-error-reports branch from 88c78a6 to fa43765 Compare October 15, 2024 20:11
Copy link
Collaborator

@whatyouhide whatyouhide left a comment

Choose a reason for hiding this comment

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

We're ready for docs for this and to :shipit:


client_report = %ClientReport{
timestamp: "2024-10-12T13:21:13",
discarded_events: [%{reason: :event_processor, category: "error"}]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Doesn't this need a quantity as well?

@savhappy savhappy requested a review from whatyouhide October 17, 2024 20:20
lib/sentry/client_report.ex Outdated Show resolved Hide resolved
@whatyouhide whatyouhide changed the title [WIP] Add client error reports Add support for client reports Oct 21, 2024
@savhappy
Copy link
Collaborator Author

@whatyouhide should we create a ticket that reminds us to add in spans to the client report when supported?

@whatyouhide whatyouhide merged commit 549d5be into master Oct 21, 2024
3 of 4 checks passed
@whatyouhide whatyouhide deleted the sav/client-error-reports branch October 21, 2024 08:27
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